Skip to content

Latest commit

 

History

History
355 lines (274 loc) · 10.4 KB

File metadata and controls

355 lines (274 loc) · 10.4 KB
outline
2
3
title PROXY_OUT Annotation
titleTemplate NpgsqlRest
description Execute PostgreSQL function first, then forward the result to an upstream service. Build post-processing pipelines with PDF rendering, ML inference, and more.
head
meta
name content
keywords
npgsqlrest proxy_out, forward proxy postgresql, post-execution proxy, upstream service, pdf rendering, ml inference
meta
property content
og:title
NpgsqlRest PROXY_OUT Annotation
meta
property content
og:description
Execute PostgreSQL function first, then forward the result to an upstream service.
meta
property content
og:type
article

PROXY_OUT

::: info Also known as forward_proxy (with or without @ prefix) :::

::: tip Available since version 3.11.0 :::

Execute the PostgreSQL function first, then forward its result body to an upstream service. The upstream response is returned to the client.

Syntax

@proxy_out
@proxy_out [ host_url ]
@proxy_out [ http_method ]
@proxy_out [ http_method ] [ host_url ]

Description

The proxy_out annotation reverses the flow of the existing proxy annotation. Instead of forwarding the incoming request to upstream, proxy_out forwards the outgoing function result.

This enables a common pattern where business logic in PostgreSQL prepares a payload, and an external service performs processing that PostgreSQL cannot do — PDF rendering, image processing, ML inference, email sending, etc.

Client Request → NpgsqlRest
  → Execute PostgreSQL function
  → Forward function result as request body to upstream service
  → Append the original request path and query string to the upstream host
  → Return upstream response to client

The client-facing HTTP method and the upstream HTTP method are independent — the client can send a GET while the upstream receives a POST.

The upstream URL is built the same way as for proxy — the incoming request path and query string are appended to the host:

target URL = host + incoming request path + incoming query string

The difference from proxy is only the direction of the body: proxy_out sends the function's result as the request body to the upstream, whereas proxy sends the incoming request body.

Basic Usage

create function generate_report(report_id int)
returns json
language plpgsql as $$
begin
    return json_build_object(
        'title', 'Monthly Report',
        'data', (select json_agg(row_to_json(t)) from sales t where month = report_id)
    );
end;
$$;

comment on function generate_report(int) is 'HTTP GET
@proxy_out POST https://render-service.internal/render';

Equivalent as a SQL file endpoint (sql/generate-report.sql):

/*
HTTP GET
@proxy_out POST https://render-service.internal/render
@param $1 report_id
*/
select json_build_object(
    'title', 'Monthly Report',
    'data', (select json_agg(row_to_json(t)) from sales t where month = $1)
);

The client calls GET /api/generate-report/?reportId=3. The server:

  1. Executes generate_report(3) in PostgreSQL.
  2. Takes the returned JSON and POSTs it to https://render-service.internal/render/api/generate-report/?reportId=3 (original query string forwarded).
  3. Returns the upstream response (e.g., a rendered PDF) directly to the client with the upstream's content-type and status code.

Proxy Annotations

Basic Proxy Out with Default Host

Uses the host from ProxyOptions.Host configuration:

-- function form
comment on function my_func() is '@proxy_out';
-- sql/my-func.sql (SQL file form)
-- @proxy_out
select my_func();

Proxy Out with Custom Host

Override the default host:

-- function form
comment on function my_func() is 'HTTP GET
@proxy_out POST https://my-other-service.internal';
-- sql/my-func.sql (SQL file form)
/*
HTTP GET
@proxy_out POST https://my-other-service.internal
*/
select my_func();

Proxy Out with HTTP Method Override

Specify which HTTP method to use for the upstream request (uses default host from configuration):

-- function form
comment on function my_func() is 'HTTP GET
@proxy_out PUT';
-- sql/my-func.sql (SQL file form)
/*
HTTP GET
@proxy_out PUT
*/
select my_func();

The client sends GET, but the upstream receives PUT with the function's result as the body.

Combined Method and Host

Specify both HTTP method and host:

-- function form
comment on function my_func() is 'HTTP GET
@proxy_out POST https://render-service.internal/render';
-- sql/my-func.sql (SQL file form)
/*
HTTP GET
@proxy_out POST https://render-service.internal/render
*/
select my_func();

Self-Referencing Proxy Out (Relative Path)

Use a relative path starting with / to forward the function result to another endpoint on the same server:

-- function form
comment on function my_func() is 'HTTP GET
@proxy_out POST /api/internal-processor';
-- sql/my-func.sql (SQL file form)
/*
HTTP GET
@proxy_out POST /api/internal-processor
*/
select my_func();

Self-referencing calls bypass the HTTP stack entirely — the target endpoint handler is invoked directly in-process with zero network overhead.

URL Resolution

The target URL follows the same resolution rules as proxy:

  • Annotation URL takes priority — if provided (absolute or relative), the global ProxyOptions.Host is ignored.
  • Global ProxyOptions.Host is used only when the annotation has no URL.
  • A relative path (starting with /) always creates an internal self-call, regardless of ProxyOptions.Host.

Path and Query String Forwarding

The original client request path and query string are both appended to the upstream host as-is (host + path + query). This lets the upstream receive the same path and parameters that were used to invoke the function:

create function generate_report(p_format text, p_id int)
returns json
language plpgsql as $$
begin
    return json_build_object('id', p_id, 'data', 'report');
end;
$$;

comment on function generate_report(text, int) is 'HTTP GET
@proxy_out POST';

With ProxyOptions.Host = "https://api.example.com", calling GET /api/generate-report/?pFormat=pdf&pId=123 executes the function, then POSTs the result body to https://api.example.com/api/generate-report/?pFormat=pdf&pId=123 — both the path and query string are appended.

To send the result to a fixed upstream path instead, put it in the annotation host (e.g. @proxy_out POST https://api.example.com/render, which forwards to https://api.example.com/render/api/generate-report/?...), or change the endpoint path with HTTP <method> <path>.

::: tip Self-calls are the exception For a relative self-call (host starting with /, e.g. @proxy_out POST /api/processor), the annotation path is the full target and the incoming request path is not appended. :::

Error Handling

  • If the function fails (database error, exception), the error is returned directly to the client — the proxy call is never made.
  • If the upstream fails (5xx, timeout, connection error), the upstream's error status and body are forwarded to the client (502 for connection errors, 504 for timeouts).

Examples

PDF Rendering Pipeline

Prepare data in PostgreSQL and render it as a PDF via an external service:

create function invoice_pdf(invoice_id int)
returns json
language plpgsql as $$
begin
    return json_build_object(
        'invoice_number', invoice_id,
        'items', (select json_agg(row_to_json(i)) from invoice_items i where i.invoice_id = invoice_pdf.invoice_id),
        'total', (select sum(amount) from invoice_items where invoice_items.invoice_id = invoice_pdf.invoice_id)
    );
end;
$$;

comment on function invoice_pdf(int) is 'HTTP GET
@proxy_out POST https://pdf-service.internal/render';

ML Inference

Send prepared feature data to an ML service:

create function predict_churn(customer_id int)
returns json
language plpgsql as $$
begin
    return (
        select json_build_object(
            'features', json_build_object(
                'total_orders', count(*),
                'last_order_days', extract(day from now() - max(order_date)),
                'avg_order_value', avg(total)
            )
        )
        from orders
        where orders.customer_id = predict_churn.customer_id
    );
end;
$$;

comment on function predict_churn(int) is 'HTTP GET
@proxy_out POST https://ml-service.internal/predict/churn';

Email Sending

Prepare email content in PostgreSQL and send via an email service:

create function send_welcome_email(user_id int)
returns json
language plpgsql as $$
declare
    u record;
begin
    select * into u from users where id = user_id;
    return json_build_object(
        'to', u.email,
        'subject', 'Welcome to Our Platform',
        'body', format('Hello %s, welcome!', u.display_name)
    );
end;
$$;

comment on function send_welcome_email(int) is 'HTTP POST
@proxy_out POST https://email-service.internal/send';

TypeScript Client

The TypeScript client generator (NpgsqlRest.TsClient) recognizes proxy_out endpoints and generates functions that return the raw Response object. Since the actual response comes from the upstream proxy service (not from the PostgreSQL function's return type), the generated function returns Promise<Response>:

// Generated for a proxy_out endpoint
export async function generateReport() : Promise<Response> {
    const response = await fetch(baseUrl + "/api/generate-report", {
        method: "GET",
    });
    return response;
}

This allows the caller to handle the response appropriately (.json(), .blob(), .text(), etc.).

Configuration

Uses the same ProxyOptions configuration as the existing proxy annotation. ProxyOptions.Enabled must be true:

{
  "NpgsqlRest": {
    "ProxyOptions": {
      "Enabled": true,
      "Host": "https://api.example.com",
      "DefaultTimeout": "30 seconds"
    }
  }
}

See Proxy Options for complete configuration reference.

Related

See Also