From 0ab6c83bc2333b20c2e407991fc50a942c793294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Bilopavlovi=C4=87?= Date: Tue, 23 Jun 2026 14:06:57 +0200 Subject: [PATCH] feat(proxy): forward all automatic parameters to proxy upstream consistently (v3.18.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an endpoint is a proxy, all server-filled parameters now forward to the upstream through one unified path: user claims, IP address, HTTP Custom Type fields, and resolved-parameter expressions. Placement mirrors the endpoint's own signature, NOT the HTTP verb: - a @body_parameter_name param carries the raw request body; - otherwise RequestParamType decides — QueryString appends to the proxy query string, BodyJson merges into the proxy JSON body (typed: numbers / booleans / embedded json / strings) when the method carries a body. Additive: the verbatim incoming request is still forwarded; the automatic params are added on top so the upstream receives the same parameter set the routine would. The previous claim/IP-only query append in BuildTargetUrl is removed and folded into the unified mechanism. Behavior change: claims/IP now follow RequestParamType (unchanged for GET/QueryString — still query; for BodyJson endpoints they now go to the body). Notes: body merge only for JSON content types (multipart/non-JSON forwarded verbatim); only expanded per-field HTTP-type params forwarded. Tests cover GET-query, POST-body (typed), param_type-query on POST, and resolved-param forwarding; existing claim/IP proxy tests pass unchanged. Full suite green (2288). See changelog/v3.18.1.md. --- NpgsqlRest/Proxy/ProxyRequestHandler.cs | 226 ++++++++++++--- .../ProxyTests/ProxyHttpTypeProbeTest.cs | 272 ++++++++++++++++++ changelog/v3.18.0.md | 2 +- changelog/v3.18.1.md | 40 +++ npm/package.json | 2 +- version.txt | 2 +- 6 files changed, 508 insertions(+), 36 deletions(-) create mode 100644 NpgsqlRestTests/ProxyTests/ProxyHttpTypeProbeTest.cs create mode 100644 changelog/v3.18.1.md diff --git a/NpgsqlRest/Proxy/ProxyRequestHandler.cs b/NpgsqlRest/Proxy/ProxyRequestHandler.cs index 856982ae..eedd8d81 100644 --- a/NpgsqlRest/Proxy/ProxyRequestHandler.cs +++ b/NpgsqlRest/Proxy/ProxyRequestHandler.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Text; +using System.Text.Json.Nodes; using Npgsql; using NpgsqlRest.HttpClientType; @@ -69,12 +70,49 @@ public static async Task InvokeAsync( // Build the target URL with user claim parameters var targetUrl = isSelfCall && _selfClient is not null - ? BuildSelfTargetUrl(host, context.Request, parameters) - : BuildTargetUrl(host, context.Request, parameters); + ? BuildSelfTargetUrl(host) + : BuildTargetUrl(host, context.Request); // Determine HTTP method var method = endpoint.ProxyMethod?.ToString().ToUpperInvariant() ?? context.Request.Method; + // Forward server-filled HTTP Custom Type field params to the upstream the same way the + // endpoint takes its own parameters. Placement mirrors the endpoint, NOT the HTTP verb: + // - a param designated as the body parameter (@body_parameter_name) carries the raw body; + // - otherwise RequestParamType decides: BodyJson -> merged JSON body (when the proxy method + // can physically carry a JSON body), QueryString -> query string. + // Additive — the verbatim incoming request is still forwarded; this only adds filled values. + string? effectiveBody = requestBody; + var forwardParams = GetForwardableParams(endpoint, parameters); + if (forwardParams is not null) + { + var bodyParam = FindForwardableBodyParam(endpoint, forwardParams); + if (bodyParam is not null) + { + if (bodyParam.Value is not null && bodyParam.Value != DBNull.Value) + { + effectiveBody = bodyParam.Value.ToString(); + } + forwardParams.Remove(bodyParam); + } + + if (forwardParams.Count > 0) + { + bool mergeIntoBody = bodyParam is null + && endpoint.RequestParamType == RequestParamType.BodyJson + && HasRequestBody(method) + && IsJsonContentType(context.Request.ContentType); + if (mergeIntoBody) + { + effectiveBody = MergeParamsIntoJsonBody(effectiveBody, forwardParams); + } + else + { + targetUrl = AppendParamsToQuery(targetUrl, forwardParams); + } + } + } + Logger?.LogDebug("Proxy starting {Method} request to '{Url}'", method, targetUrl); try @@ -108,7 +146,7 @@ public static async Task InvokeAsync( method, targetUrl, proxyHeaders, - requestBody, + effectiveBody, context.Request.ContentType, cancellationToken); @@ -127,7 +165,7 @@ public static async Task InvokeAsync( }; } - using var request = await CreateRequestAsync(context, method, targetUrl, requestBody, proxyOptions, userContextHeaders); + using var request = await CreateRequestAsync(context, method, targetUrl, effectiveBody, proxyOptions, userContextHeaders); using var cts = CreateTimeoutCancellationTokenSource(proxyOptions.DefaultTimeout, cancellationToken); var client = isSelfCall && _selfClient is not null ? _selfClient : SharedClient; @@ -171,56 +209,178 @@ public static async Task InvokeAsync( /// /// Build target URL for self-referencing proxy calls. Uses the host as the full path (no appending of request path). /// - private static string BuildSelfTargetUrl(string host, HttpRequest request, NpgsqlParameterCollection? parameters) + private static string BuildSelfTargetUrl(string host) { // For self-calls, host IS the full relative path (e.g., /api/hello-world) // Don't append the incoming request path return host; } - private static string BuildTargetUrl(string host, HttpRequest request, NpgsqlParameterCollection? parameters) + private static string BuildTargetUrl(string host, HttpRequest request) { // Ensure host doesn't end with / host = host.TrimEnd('/'); - // Get the path and query string + // Get the path and query string. Automatic (server-filled) parameters — user claims, IP, + // HTTP Custom Type fields, resolved-parameter expressions — are appended consistently by the + // unified forwarding step in InvokeAsync (query string or JSON body, per the endpoint shape). var path = request.Path.Value ?? ""; var queryString = request.QueryString.Value ?? ""; - // Append user claim and IP address parameters to query string - if (parameters is not null) + return $"{host}{path}{queryString}"; + } + + private static bool IsJsonContentType(string? contentType) => + contentType is not null && contentType.Contains("json", StringComparison.OrdinalIgnoreCase); + + /// + /// True when a parameter is filled by the server (not supplied by the client) and so should be + /// forwarded to the proxy upstream the same way the endpoint receives it. All automatic parameter + /// sources are treated consistently: user claims, IP address, HTTP Custom Type fields (the expanded + /// per-field params on DB functions), and resolved-parameter expressions. Single-composite HTTP + /// params (SQL-file shape, CustomTypeName null) are not forwarded. + /// + private static bool IsAutomaticParam(RoutineEndpoint endpoint, NpgsqlRestParameter p) + { + if (p.IsFromUserClaims || p.IsIpAddress) { - var additionalParams = new StringBuilder(); - foreach (NpgsqlParameter param in parameters) + return true; + } + if (p.TypeDescriptor.CustomType is not null + && p.TypeDescriptor.CustomTypeName is not null + && HttpClientTypes.Definitions.ContainsKey(p.TypeDescriptor.CustomType)) + { + return true; + } + if (endpoint.ResolvedParameterExpressions is not null + && (endpoint.ResolvedParameterExpressions.ContainsKey(p.ActualName) + || endpoint.ResolvedParameterExpressions.ContainsKey(p.ConvertedName))) + { + return true; + } + return false; + } + + /// + /// The forwardable parameter (if any) designated as the request body parameter + /// (@body_parameter_name) — it carries the raw request body rather than a query/JSON field. + /// + private static NpgsqlRestParameter? FindForwardableBodyParam(RoutineEndpoint endpoint, List parameters) + { + if (!endpoint.HasBodyParameter) + { + return null; + } + foreach (var p in parameters) + { + if (string.Equals(endpoint.BodyParameterName, p.ConvertedName, StringComparison.Ordinal) + || string.Equals(endpoint.BodyParameterName, p.ActualName, StringComparison.Ordinal)) { - if (param is NpgsqlRestParameter restParam && - (restParam.IsFromUserClaims || restParam.IsIpAddress) && - restParam.Value is not null && restParam.Value != DBNull.Value) - { - if (additionalParams.Length > 0) - { - additionalParams.Append('&'); - } - additionalParams.Append(Uri.EscapeDataString(restParam.ConvertedName)); - additionalParams.Append('='); - additionalParams.Append(Uri.EscapeDataString(restParam.Value.ToString() ?? "")); - } + return p; } + } + return null; + } - if (additionalParams.Length > 0) + /// + /// Collects all server-filled (automatic) parameters to forward to the proxy upstream. + /// + private static List? GetForwardableParams(RoutineEndpoint endpoint, NpgsqlParameterCollection? parameters) + { + if (parameters is null) + { + return null; + } + List? result = null; + for (var i = 0; i < parameters.Count; i++) + { + if (parameters[i] is NpgsqlRestParameter rp && IsAutomaticParam(endpoint, rp)) { - if (string.IsNullOrEmpty(queryString)) - { - queryString = "?" + additionalParams.ToString(); - } - else - { - queryString = queryString + "&" + additionalParams.ToString(); - } + (result ??= []).Add(rp); } } + return result; + } - return $"{host}{path}{queryString}"; + private static string AppendParamsToQuery(string url, List parameters) + { + var sb = new StringBuilder(); + foreach (var p in parameters) + { + if (p.Value is null || p.Value == DBNull.Value) + { + continue; + } + if (sb.Length > 0) + { + sb.Append('&'); + } + sb.Append(Uri.EscapeDataString(p.ConvertedName)); + sb.Append('='); + sb.Append(Uri.EscapeDataString(p.Value.ToString() ?? "")); + } + if (sb.Length == 0) + { + return url; + } + return url.Contains('?') ? $"{url}&{sb}" : $"{url}?{sb}"; + } + + private static string MergeParamsIntoJsonBody(string? body, List parameters) + { + JsonObject obj; + if (string.IsNullOrWhiteSpace(body)) + { + obj = []; + } + else + { + try + { + obj = JsonNode.Parse(body) as JsonObject ?? []; + } + catch + { + obj = []; + } + } + foreach (var p in parameters) + { + obj[p.ConvertedName] = ToJsonNode(p); + } + return obj.ToJsonString(); + } + + // Typed JSON value for a server-filled HTTP-type field, following the field's declared type: + // numeric/boolean fields become JSON numbers/bools, json fields are embedded as JSON, the rest + // become JSON strings. The value was already typed by the HTTP-type fill (asText vs native). + private static JsonNode? ToJsonNode(NpgsqlRestParameter p) + { + var v = p.Value; + if (v is null || v == DBNull.Value) + { + return null; + } + if (p.TypeDescriptor.IsJson) + { + var s = v.ToString(); + if (string.IsNullOrEmpty(s)) + { + return null; + } + try { return JsonNode.Parse(s); } + catch { return JsonValue.Create(s); } + } + return v switch + { + bool b => JsonValue.Create(b), + int i => JsonValue.Create(i), + long l => JsonValue.Create(l), + short sh => JsonValue.Create((int)sh), + decimal dec => JsonValue.Create(dec), + double d => JsonValue.Create(d), + _ => JsonValue.Create(v.ToString()) + }; } private static async Task CreateRequestAsync( diff --git a/NpgsqlRestTests/ProxyTests/ProxyHttpTypeProbeTest.cs b/NpgsqlRestTests/ProxyTests/ProxyHttpTypeProbeTest.cs new file mode 100644 index 00000000..d29fac17 --- /dev/null +++ b/NpgsqlRestTests/ProxyTests/ProxyHttpTypeProbeTest.cs @@ -0,0 +1,272 @@ +using WireMock.Server; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock; +using WireMock.Types; +using WireMock.Util; + +namespace NpgsqlRestTests; + +public static partial class Database +{ + public static void ProxyHttpTypeProbeTest() + { + // Functions that have BOTH an HTTP Custom Type parameter AND a (passthrough) @proxy. + // The HTTP type points at the HttpClientType WireMock (port 50953); the @proxy (no host) + // forwards to ProxyOptions.Host = http://localhost:50954, preserving the incoming path. + // The function bodies raise, to prove they are NOT executed (passthrough). + script.Append($@" + create type proxy_httptype_probe as ( + body text, + status_code int, + success boolean, + error_message text + ); + comment on type proxy_httptype_probe is 'GET http://localhost:{WireMockFixture.Port}/httptype-source'; + + -- GET endpoint: server-filled HTTP-type fields are forwarded on the proxy QUERY STRING. + create function proxy_with_httptype( + _response proxy_httptype_probe default null + ) + returns void + language plpgsql + as + $$ + begin + raise exception 'passthrough proxy must NOT execute the function'; + end; + $$; + comment on function proxy_with_httptype(proxy_httptype_probe) is 'HTTP GET +allow_anonymous +proxy'; + + -- POST endpoint: server-filled HTTP-type fields are merged into the proxy JSON BODY, + -- alongside the client's original body fields. + create function proxy_post_with_httptype( + _payload text default null, + _response proxy_httptype_probe default null + ) + returns void + language plpgsql + as + $$ + begin + raise exception 'passthrough proxy must NOT execute the function'; + end; + $$; + comment on function proxy_post_with_httptype(text, proxy_httptype_probe) is 'HTTP POST +allow_anonymous +proxy'; + + -- POST endpoint, but params come from the QUERY STRING (param_type query). Placement must + -- follow RequestParamType, not the verb: the HTTP-type fields go to the proxy QUERY even + -- though the method is POST and a JSON body is sent. + create function proxy_post_query_with_httptype( + _response proxy_httptype_probe default null + ) + returns void + language plpgsql + as + $$ + begin + raise exception 'passthrough proxy must NOT execute the function'; + end; + $$; + comment on function proxy_post_query_with_httptype(proxy_httptype_probe) is 'HTTP POST +allow_anonymous +param_type query +proxy'; + + -- Resolved-parameter expression: _token is filled server-side from a DB lookup. It is an + -- automatic parameter and must be forwarded to the proxy just like claims / IP / HTTP-type. + create table proxy_resolved_tokens (user_name text primary key, api_token text not null); + insert into proxy_resolved_tokens values ('alice', 'TOKEN-ALICE-123'); + + create function proxy_resolved_param( + _name text, + _token text default null + ) + returns void + language plpgsql + as + $$ + begin + raise exception 'passthrough proxy must NOT execute the function'; + end; + $$; + comment on function proxy_resolved_param(text, text) is 'HTTP GET +allow_anonymous +_token = select api_token from proxy_resolved_tokens where user_name = {{_name}} +proxy'; +"); + } +} + +[Collection("TestFixture")] +public class ProxyHttpTypeProbeTest : IClassFixture, IClassFixture +{ + private readonly TestFixture _test; + private readonly WireMockServer _httpTypeServer; // 50953 — the HTTP Custom Type target + private readonly WireMockServer _proxyServer; // 50954 — the @proxy target + + public ProxyHttpTypeProbeTest(TestFixture test, WireMockFixture httpType, ProxyWireMockFixture proxy) + { + _test = test; + _httpTypeServer = httpType.Server; + _proxyServer = proxy.Server; + _httpTypeServer.Reset(); + _proxyServer.Reset(); + } + + // GET proxy: the auto-filled HTTP-type fields are forwarded on the proxy's QUERY STRING, + // alongside the client's original query — "same signature, sent upstream". + [Fact] + public async Task HttpType_fields_are_forwarded_on_proxy_query_for_get() + { + int httpTypeCalls = 0; + _httpTypeServer + .Given(Request.Create().WithPath("/httptype-source").UsingGet()) + .RespondWith(Response.Create().WithCallback(req => + { + Interlocked.Increment(ref httpTypeCalls); + return new ResponseMessage + { + StatusCode = 200, + BodyData = new BodyData { BodyAsString = "FETCHED-FROM-HTTPTYPE", DetectedBodyType = BodyType.String } + }; + })); + + // Proxy target echoes the exact URL (incl. query) it received. + _proxyServer + .Given(Request.Create().WithPath("/api/proxy-with-httptype/").UsingGet()) + .RespondWith(Response.Create().WithCallback(req => + new ResponseMessage + { + StatusCode = 200, + BodyData = new BodyData { BodyAsString = $"PROXY-URL:[{req.Url}]", DetectedBodyType = BodyType.String } + })); + + using var response = await _test.Client.GetAsync("/api/proxy-with-httptype/?clientParam=XYZ"); + var content = await response.Content.ReadAsStringAsync(); + + response.StatusCode.Should().Be(HttpStatusCode.OK); // proxy response (function never ran) + content.Should().StartWith("PROXY-URL:["); + httpTypeCalls.Should().Be(1); // HTTP-type call fired + + // The proxy received the client's original query … + content.Should().Contain("clientParam=XYZ"); + // … AND the auto-filled HTTP-type fields: + content.Should().Contain("responseBody=FETCHED-FROM-HTTPTYPE"); + content.Should().Contain("responseStatusCode=200"); + } + + // POST proxy: the auto-filled HTTP-type fields are merged into the proxy JSON BODY, alongside the + // client's original body fields — and a large body field never ends up in the URL. + [Fact] + public async Task HttpType_fields_are_merged_into_proxy_json_body_for_post() + { + int httpTypeCalls = 0; + _httpTypeServer + .Given(Request.Create().WithPath("/httptype-source").UsingGet()) + .RespondWith(Response.Create().WithCallback(req => + { + Interlocked.Increment(ref httpTypeCalls); + return new ResponseMessage + { + StatusCode = 200, + BodyData = new BodyData { BodyAsString = "FETCHED-FROM-HTTPTYPE", DetectedBodyType = BodyType.String } + }; + })); + + // Proxy target echoes the exact body it received. + _proxyServer + .Given(Request.Create().WithPath("/api/proxy-post-with-httptype/").UsingPost()) + .RespondWith(Response.Create().WithCallback(req => + new ResponseMessage + { + StatusCode = 200, + BodyData = new BodyData { BodyAsString = $"PROXY-BODY:[{req.Body}]", DetectedBodyType = BodyType.String } + })); + + using var response = await _test.Client.PostAsync( + "/api/proxy-post-with-httptype/", + new StringContent("{\"payload\": \"CLIENT\"}", System.Text.Encoding.UTF8, "application/json")); + var content = await response.Content.ReadAsStringAsync(); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + httpTypeCalls.Should().Be(1); + + // The client's original body field is preserved … + content.Should().Contain("\"payload\":\"CLIENT\""); + // … and the auto-filled HTTP-type fields are merged in, typed (string / number / bool): + content.Should().Contain("\"responseBody\":\"FETCHED-FROM-HTTPTYPE\""); + content.Should().Contain("\"responseStatusCode\":200"); + content.Should().Contain("\"responseSuccess\":true"); + } + + // Placement follows RequestParamType, NOT the HTTP verb. This POST endpoint is configured with + // `param_type query`, so even though the method is POST and the client sends a JSON body, the + // HTTP-type fields must go to the proxy QUERY STRING — not merged into the body. + [Fact] + public async Task HttpType_fields_follow_request_param_type_not_verb() + { + int httpTypeCalls = 0; + _httpTypeServer + .Given(Request.Create().WithPath("/httptype-source").UsingGet()) + .RespondWith(Response.Create().WithCallback(req => + { + Interlocked.Increment(ref httpTypeCalls); + return new ResponseMessage + { + StatusCode = 200, + BodyData = new BodyData { BodyAsString = "FETCHED-FROM-HTTPTYPE", DetectedBodyType = BodyType.String } + }; + })); + + // Proxy target echoes both the URL and the body it received. + _proxyServer + .Given(Request.Create().WithPath("/api/proxy-post-query-with-httptype/").UsingPost()) + .RespondWith(Response.Create().WithCallback(req => + new ResponseMessage + { + StatusCode = 200, + BodyData = new BodyData { BodyAsString = $"URL:[{req.Url}] BODY:[{req.Body}]", DetectedBodyType = BodyType.String } + })); + + using var response = await _test.Client.PostAsync( + "/api/proxy-post-query-with-httptype/", + new StringContent("{\"ignored\": \"CLIENTBODY\"}", System.Text.Encoding.UTF8, "application/json")); + var content = await response.Content.ReadAsStringAsync(); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + httpTypeCalls.Should().Be(1); + + // HTTP-type fields forwarded on the QUERY (param_type query), in query form … + content.Should().Contain("responseBody=FETCHED-FROM-HTTPTYPE"); + // … and NOT merged into the JSON body (which is forwarded verbatim). + content.Should().NotContain("\"responseBody\":"); + content.Should().Contain("CLIENTBODY"); + } + + // Consistency: a resolved-parameter expression is an automatic parameter too, so it is forwarded + // to the proxy exactly like claims / IP / HTTP-type fields (here: a GET → query string). + [Fact] + public async Task Resolved_param_is_forwarded_to_proxy_consistently() + { + _proxyServer + .Given(Request.Create().WithPath("/api/proxy-resolved-param/").UsingGet()) + .RespondWith(Response.Create().WithCallback(req => + new ResponseMessage + { + StatusCode = 200, + BodyData = new BodyData { BodyAsString = $"PROXY-URL:[{req.Url}]", DetectedBodyType = BodyType.String } + })); + + using var response = await _test.Client.GetAsync("/api/proxy-resolved-param/?name=alice"); + var content = await response.Content.ReadAsStringAsync(); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + content.Should().Contain("name=alice"); // client query forwarded verbatim + content.Should().Contain("token=TOKEN-ALICE-123"); // resolved (automatic) param forwarded + } +} diff --git a/changelog/v3.18.0.md b/changelog/v3.18.0.md index 33042a0a..aa6c08b0 100644 --- a/changelog/v3.18.0.md +++ b/changelog/v3.18.0.md @@ -1,6 +1,6 @@ # Changelog v3.18.0 -## Version [3.18.0](https://github.com/NpgsqlRest/NpgsqlRest/tree/3.18.0) +## Version [3.18.0](https://github.com/NpgsqlRest/NpgsqlRest/tree/3.18.0) (2026-06-23) [Full Changelog](https://github.com/NpgsqlRest/NpgsqlRest/compare/3.17.0...3.18.0) diff --git a/changelog/v3.18.1.md b/changelog/v3.18.1.md new file mode 100644 index 00000000..6719cbe8 --- /dev/null +++ b/changelog/v3.18.1.md @@ -0,0 +1,40 @@ +# Changelog v3.18.1 + +## Version [3.18.1](https://github.com/NpgsqlRest/NpgsqlRest/tree/3.18.1) (2026-06-23) + +[Full Changelog](https://github.com/NpgsqlRest/NpgsqlRest/compare/3.18.0...3.18.1) + +Patch release that makes **all automatic (server-filled) parameters forward to proxy endpoints consistently**, in the endpoint's native parameter shape. + +## What changed + +When an endpoint is a proxy, the parameters that NpgsqlRest fills server-side are now forwarded to the upstream **uniformly**, regardless of source: + +- **user claims** (claim-mapped parameters), +- **IP address** parameter, +- **HTTP Custom Type** fields (the auto-filled `responseBody` / `responseStatusCode` / … on a routine with an HTTP Custom Type parameter), +- **resolved-parameter expressions** (values looked up server-side via SQL). + +All of them follow the **same placement rule**, which mirrors how the endpoint itself receives parameters — **not** the HTTP verb: + +- The parameter designated as the body parameter (`@body_parameter_name`) carries the **raw request body**. +- Otherwise placement follows the endpoint's **`RequestParamType`**: `QueryString` → appended to the proxy **query string**; `BodyJson` → merged into the proxy **JSON body** (typed: numbers, booleans, embedded JSON, or strings), when the proxy method can carry a JSON body. + +This is **additive**: the verbatim incoming request is still forwarded; the automatic parameters are added on top, so the upstream receives the same parameter set the routine would have. + +### Why + +Previously the behavior was inconsistent: user-claim and IP parameters were always appended to the query string, HTTP Custom Type fields and resolved parameters were not forwarded at all, and a passthrough proxy discarded the auto-filled values entirely (the outbound HTTP Custom Type call fired but its result went nowhere). Now every automatic parameter behaves the same way. + +### Behavior change to note + +User-claim and IP parameters now follow `RequestParamType` like every other automatic parameter. For a `QueryString` endpoint (the default for GET) they remain in the query string, exactly as before. For a `BodyJson` endpoint they are now merged into the JSON body rather than forced onto the query string. Method does not decide placement — `RequestParamType` does (a POST endpoint can use `param_type query` and its parameters then go to the query string). + +### Notes + +- Body merging applies only when the forwarded request carries a **JSON** content type; multipart and non-JSON bodies are forwarded verbatim. +- Only the expanded per-field HTTP Custom Type parameters (DB-function shape) are forwarded; single-composite HTTP parameters (SQL-file shape) are not. + +## Tests + +`NpgsqlRestTests/ProxyTests/ProxyHttpTypeProbeTest.cs` covers, via a WireMock proxy target that echoes the received URL / body: HTTP-type fields forwarded on the query (GET) and merged into the JSON body (POST, typed); placement following `RequestParamType` rather than the verb (a `param_type query` POST forwards to the query, not the body); and a resolved-parameter expression forwarded consistently. Existing user-claim / IP proxy tests continue to pass unchanged (GET → query string). Full suite green (2288). diff --git a/npm/package.json b/npm/package.json index 621ecdbe..6fd5bf82 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "npgsqlrest", - "version": "3.18.0", + "version": "3.18.1", "description": "Automatic REST API for PostgreSQL Databases Client Build", "scripts": { "postinstall": "node postinstall.js", diff --git a/version.txt b/version.txt index ae561550..10724e0b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.18.0 \ No newline at end of file +3.18.1 \ No newline at end of file