reference: media type handlers

This commit is contained in:
steve-chavez
2023-11-30 23:27:06 -05:00
committed by Steve Chavez
parent 62512268cd
commit d15e357d2e
12 changed files with 337 additions and 166 deletions
+11 -13
View File
@@ -5,22 +5,20 @@ Create a SOAP endpoint
:author: `fjf2002 <https://github.com/fjf2002>`_
PostgREST now has XML support. With a bit of work, SOAP endpoints become possible.
Please note that PostgREST supports just ``text/xml`` MIME type in request/response headers ``Content-Type`` and ``Accept``.
If you have to use other MIME types such as ``application/soap+xml``, you could manipulate the headers in your reverse proxy.
PostgREST supports :ref:`custom_media`. With a bit of work, SOAP endpoints become possible.
Minimal Example
---------------
This example will simply return the request body, inside a tag ``therequestbodywas``.
Add the following function to your PostgreSQL database:
.. code-block:: postgres
CREATE OR REPLACE FUNCTION my_soap_endpoint(xml) RETURNS xml AS $$
create domain "text/xml" as pg_catalog.xml;
CREATE OR REPLACE FUNCTION my_soap_endpoint(xml) RETURNS "text/xml" AS $$
DECLARE
nsarray CONSTANT text[][] := ARRAY[
ARRAY['soapenv', 'http://schemas.xmlsoap.org/soap/envelope/']
@@ -121,7 +119,7 @@ potentially disclosing internals to the client, but instead handle the errors di
xmlelement(NAME "soapenv:Body", body)
);
$function$;
-- helper function
CREATE OR REPLACE FUNCTION _soap_exception(
faultcode text,
@@ -137,9 +135,9 @@ potentially disclosing internals to the client, but instead handle the errors di
)
);
$function$;
CREATE OR REPLACE FUNCTION fraction_to_decimal(xml)
RETURNS xml
RETURNS "text/xml"
LANGUAGE plpgsql
AS $function$
DECLARE
@@ -207,14 +205,14 @@ The output should roughly look like:
</soapenv:Body>
</soapenv:Envelope>
References
----------
For more information concerning PostgREST, cf.
- :ref:`s_proc_single_unnamed`
- :ref:`scalar_return_formats`
- :ref:`Nginx reverse proxy <admin>`
- :ref:`custom_media`. See :ref:`any_handler`, if you need to support an ``application/soap+xml`` media type.
- :ref:`Nginx reverse proxy <nginx>`
For SOAP reference, visit
@@ -23,7 +23,7 @@ To simplify things, we won't be using authentication, so grant all permissions o
grant all on api.todos to web_anon;
grant usage, select on sequence api.todos_id_seq to web_anon;
Next, add the ``text/html`` media type as a DOMAIN. With this, PostgREST can identify the request made by your web browser (with the ``Accept: text/html`` header)
Next, add the ``text/html`` as a :ref:`custom_media`. With this, PostgREST can identify the request made by your web browser (with the ``Accept: text/html`` header)
and return a raw HTML document file.
.. code-block:: postgres
+36 -10
View File
@@ -26,18 +26,42 @@ First, we need a public table for storing the files.
, blob bytea
);
Let's assume this table contains an image of two cute kittens with id 42.
We can retrieve this image in binary format from our PostgREST API by requesting :code:`/files?select=blob&id=eq.42` with the :code:`Accept: application/octet-stream` header.
Unfortunately, putting the URL into the :code:`src` of an :code:`<img>` tag will not work.
That's because browsers do not send the required :code:`Accept: application/octet-stream` header.
Let's assume this table contains an image of two cute kittens with id 42. We can retrieve this image in binary format from our PostgREST API by using :ref:`custom_media`:
.. code-block:: postgres
create domain "application/octet-stream" as bytea;
create or replace function file(id int) returns "application/octet-stream" as $$
select blob from files where id = file.id;
$$ language sql;
Now we can request the RPC endpoint :code:`/rpc/file?id=42` with the :code:`Accept: application/octet-stream` header.
.. code-block:: bash
curl "localhost:3000/rpc/file?id=42" -H "Accept: application/octet-stream"
Unfortunately, putting the URL into the :code:`src` of an :code:`<img>` tag will not work. That's because browsers do not send the required :code:`Accept: application/octet-stream` header.
Instead, the :code:`Accept: image/webp` header is sent by many web browsers by default.
Luckily we can change the accepted media type in the function like so:
.. code-block:: postgres
create domain "image/webp" as bytea;
create or replace function file(id int) returns "image/webp" as $$
select blob from files where id = file.id;
$$ language sql;
Luckily we can specify the accepted media types in the :ref:`raw-media-types` configuration variable.
In this case, the :code:`Accept: image/webp` header is sent by many web browsers by default, so let's add it to the configuration variable, like this: :code:`raw-media-types="image/webp"`.
Now, the image will be displayed in the HTML page:
.. code-block:: html
<img src="http://localhost:3000/files?select=blob&id=eq.42" alt="Cute Kittens"/>
<img src="http://localhost:3000/file?id=42" alt="Cute Kittens"/>
Improved Version
----------------
@@ -60,13 +84,15 @@ First, in addition to the minimal example, we need to store the media types and
add column type text,
add column name text;
Next, we set up an RPC endpoint that sets the content type and filename.
Next, we set modify the function to set the content type and filename.
We use this opportunity to configure some basic, client-side caching.
For production, you probably want to configure additional caches, e.g. on the :ref:`reverse proxy <admin>`.
.. code-block:: postgres
create function file(id int) returns bytea as
create domain "*/*" as bytea;
create function file(id int) returns "*/*" as
$$
declare headers text;
declare blob bytea;
@@ -79,7 +105,7 @@ For production, you probably want to configure additional caches, e.g. on the :r
from files where files.id = file.id into headers;
perform set_config('response.headers', headers, true);
select files.blob from files where files.id = file.id into blob;
if found
if FOUND -- special var, see https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS
then return(blob);
else raise sqlstate 'PT404' using
message = 'NOT FOUND',
@@ -506,33 +506,26 @@ Now, to send the file ``postgrest-logo.png`` we need to set the ``Content-Type:
-X POST -H "Content-Type: application/octet-stream" \
--data-binary "@postgrest-logo.png"
To get the image from the database, set the ``Accept: application/octet-stream`` header and select only the
``bytea`` type column.
To get the image from the database, use :ref:`custom_media` like so:
.. code-block:: postgres
create domain "image/png" as bytea;
create or replace get_image(id int) returns "image/png" as $$
select file from files where id = $1;
$$ language sql;
.. tabs::
.. code-tab:: http
GET /files?select=file&id=eq.1 HTTP/1.1
Accept: application/octet-stream
.. code-tab:: bash Curl
curl "http://localhost:3000/files?select=file&id=eq.1" \
-H "Accept: application/octet-stream"
Use more accurate headers according to the type of the files by using the :ref:`raw-media-types` configuration. For example, adding the ``raw-media-types="image/png"`` setting to the configuration file will allow you to use the ``Accept: image/png`` header:
.. tabs::
.. code-tab:: http
GET /files?select=file&id=eq.1 HTTP/1.1
GET /get_image?id=1 HTTP/1.1
Accept: image/png
.. code-tab:: bash Curl
curl "http://localhost:3000/files?select=file&id=eq.1" \
curl "http://localhost:3000/get_image?id=1" \
-H "Accept: image/png"
See :ref:`providing_img` for a step-by-step example on how to handle images in HTML.
+1
View File
@@ -16,6 +16,7 @@ PostgREST exposes three database objects of a schema as resources: tables, views
api/domain_representations.rst
api/resource_embedding.rst
api/resource_representation.rst
api/media_type_handlers.rst
api/openapi.rst
api/preferences.rst
api/*
+234 -18
View File
@@ -1,29 +1,245 @@
.. _custom_media:
Media Type Handlers
###################
PostgREST offers builtin handlers for common media types such as ``application/json`` and ``text/csv``. You can add handlers for other media types or override the builtin handlers.
Media Type Handlers allows PostgREST to deliver custom media types. These handlers extend the :ref:`builtin ones <builtin_media>` and can also override them.
Standard Media Types Handlers
=============================
Media types are expressed as type aliases using `domains <https://www.postgresql.org/docs/current/sql-createdomain.html>`_ and their name must comply to `RFC 6838 requirements <https://datatracker.ietf.org/doc/html/rfc6838#section-4.2>`_.
Builtins handlers are provided for the following standard media types:
.. code-block:: postgres
* ``application/json``
* ``text/csv``
* ``application/geo+json``
* ``application/x-www-form-urlencoded``
* ``*/*``, uses the same handler as ``application/json``.
CREATE DOMAIN "application/json" AS json;
Vendor Media Types Handlers
===========================
By using these domains as return types:
PostgREST also includes its own vendored media types, handlers for these are provided but cannot be overridden.
- Of :ref:`Functions <s_procs>`, these will turn into handlers.
* ``application/vnd.pgrst.object``
* ``application/vnd.pgrst.array``
* ``application/vnd.pgrst.plan``
- Of `Aggregates <https://www.postgresql.org/docs/current/sql-createaggregate.html>`_ transition or final functions, these will serve as handlers for :ref:`tables_views` and :ref:`table_functions`.
Custom Media Type Handlers
==========================
.. note::
TODO
PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overriden in this way.
Handler Function
================
As an example, let's obtain the `TWKB <https://postgis.net/docs/ST_AsTWKB.html>`_ compressed binary format for a PostGIS geometry.
.. code-block:: postgres
create extension postgis;
create table lines (
id int primary key
, name text
, geom geometry(LINESTRING, 4326)
);
insert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::extensions.geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::extensions.geometry);
For this you can create a vendor media type and use it as a return type on a function.
.. code-block:: postgres
create domain "application/vnd.twkb" as bytea;
create or replace function get_line (id int)
returns "application/vnd.twkb" as $$
select st_astwkb(geom) from lines where id = get_line.id;
$$ language sql;
.. note::
For PostgreSQL <= 12, you'll need a cast on the function body :code:`st_astwkb(geom)::"application/vnd.twkb"`.
Now you can request the ``TWKB`` output like so:
.. code-block:: bash
curl 'localhost:3000/rpc/get_line?id=1' -H "Accept: application/vnd.twkb" -i
HTTP/1.1 200 OK
Content-Type: application/vnd.twkb
# binary output
Note that PostgREST will automatically set the ``Content-Type`` to ``application/vnd.twkb``.
Handlers for Tables/Views
=========================
To benefit from a compressed format like ``TWKB``, it makes more sense to obtain many rows instead of one. Let's allow that by adding a handler for the table. You'll need an aggregate:
.. code-block:: postgres
create or replace function twkb_handler_transition (state bytea, next lines)
returns "application/vnd.twkb" as $$
select state || st_astwkb(next.geom);
$$ language sql;
create or replace aggregate twkb_agg (lines) (
initcond = ''
, stype = "application/vnd.twkb"
, sfunc = twkb_handler_transition
);
-- quick test
-- SELECT twkb_agg(l) from lines l;
-- twkb_agg
------------------------------------------------------------------
-- \xa20002c09a0cc09a0c80ea3080ea30a2000280b51880b51880ea3080ea30
--(1 row)
Now you can request the table endpoint with the ``twkb`` media type:
.. code-block:: bash
curl 'localhost:3000/lines' -H "Accept: application/vnd.twkb" -i
HTTP/1.1 200 OK
Content-Type: application/vnd.twkb
# binary output
If you have a table-valued function returning the same table type, the handler can also act upon on it.
.. code-block:: postgres
create or replace function get_lines ()
returns setof lines as $$
select * from lines;
$$ language sql;
.. code-block:: bash
curl 'localhost:3000/get_lines' -H "Accept: application/vnd.twkb" -i
HTTP/1.1 200 OK
Content-Type: application/vnd.twkb
# binary output
Overriding a Builtin Handler
============================
Let's override the existing ``text/csv`` handler for the table to provide a more complex CSV output.
It'll include a `Byte order mark <https://en.wikipedia.org/wiki/Byte_order_mark>`_ plus a ``Content-Disposition`` header to set a name for the downloaded file.
.. code-block:: postgres
create domain "text/csv" as text;
create or replace function bom_csv_trans (state text, next lines)
returns "text/csv" as $$
select state || next.id::text || ',' || next.name || ',' || next.geom::text || E'\n';
$$ language sql;
create or replace function bom_csv_final (data "text/csv")
returns "text/csv" as $$
-- set the Content-Disposition header
select set_config('response.headers', '[{"Content-Disposition": "attachment; filename=\"lines.csv\""}]', true);
select
-- EFBBBF is the BOM in UTF8 https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
convert_from (decode (E'EFBBBF', 'hex'),'UTF8') ||
-- the header for the CSV
(E'id,name,geom\n' || data);
$$ language sql;
drop aggregate if exists bom_csv_agg(lines);
create aggregate bom_csv_agg (lines) (
initcond = ''
, stype = "text/csv"
, sfunc = bom_csv_trans
, finalfunc = bom_csv_final
);
You can now request it like:
.. code-block:: bash
curl 'localhost:3000/lines' -H "Accept: text/csv" -i
HTTP/1.1 200 OK
Content-Type: text/csv
Content-Disposition: attachment; filename="lines.csv"
id,name,geom
1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440
2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840
.. _any_handler:
The "Any" Handler
=================
For more flexibility, you can also define a catch-all handler by using a domain named ``*/*`` (any media type). This will respond to all media types and even to requests that don't include an ``Accept`` header.
Note that this will take priority over all other handlers (builtin or custom), so it's better to do it for an isolated function or view.
Let's define an any handler for a view that will always respond with ``XML`` output. It will accept ``text/xml``, ``application/xml``, ``*/*`` and reject other media types.
.. code-block:: postgres
create domain "*/*" as pg_catalog.xml;
create view "lines.xml" as
select * from lines;
create or replace function lines_xml_trans (state "*/*", next "lines.xml")
returns "*/*" as $$
select xmlconcat(state, xmlelement(name line, xmlattributes(next.id as id, next.name as name), next.geom));
$$ language sql;
create or replace function lines_xml_final (data "*/*")
returns "*/*" as $$
declare
req_accept text := current_setting('request.headers', true)::json->>'accept';
begin
-- when receiving */*, we need to set the Content-Type. PostgREST won't set it.
if req_accept = '*/*'
then perform set_config('response.headers', '[{"Content-Type": "text/xml"}]', true);
-- we'll reject other non XML media types, we need to reject manually since */* will command PostgREST to accept all media types
elsif req_accept NOT IN ('application/xml', 'text/xml')
then raise sqlstate 'PT415' using message = 'Unsupported Media Type';
end if;
return data;
end; $$ language plpgsql;
drop aggregate if exists lines_xml_agg ("lines.xml");
create aggregate test.lines_xml_agg ("lines.xml") (
stype = "*/*"
, sfunc = lines_xml_trans
, finalfunc = lines_xml_final
);
Now we can omit the ``Accept`` header and it will respond with XML.
.. code-block:: bash
curl 'localhost:3000/lines.xml' -i
HTTP/1.1 200 OK
Content-Type: text/xml
<line id="1" name="line-1">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>
<line id="2" name="line-2">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>
And it will accept only XML media types.
.. code-block:: bash
curl 'localhost:3000/lines.xml' -i -H "Accept: text/xml"
HTTP/1.1 200 OK
Content-Type: text/xml
curl 'localhost:3000/lines.xml' -i -H "Accept: application/xml"
HTTP/1.1 200 OK
Content-Type: text/xml
curl 'localhost:3000/lines.xml' -i -H "Accept: unknown/media"
HTTP/1.1 415 Unsupported Media Type
+38 -73
View File
@@ -23,15 +23,44 @@ Use the Accept request header to specify the acceptable format (or formats) for
curl "http://localhost:3000/people" \
-H "Accept: application/json"
For tables and views the current possibilities are:
.. _builtin_media:
* ``*/*``
* ``text/csv``
* ``application/json``
* ``application/openapi+json``
* ``application/geo+json``
Builtin Media Type Handlers
===========================
The server will default to JSON for API endpoints and OpenAPI on the root.
Builtin handlers are offered for common standard media types.
* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`s_procs`.
* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.
* ``application/geo+json``, see :ref:`ww_postgis`.
* ``*/*``, resolves to ``application/json`` for API endpoints and to ``application/openapi+json`` for the root endpoint.
The following vendor media types handlers are also supported.
* ``application/vnd.pgrst.plan``, see :ref:`explain_plan`.
* ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``, see :ref:`singular_plural` and :ref:`stripped_nulls`.
Any unrecognized media type will throw an error.
.. tabs::
.. code-tab:: http
GET /people HTTP/1.1
Accept: unknown/unknown
.. code-tab:: bash Curl
curl "http://localhost:3000/people" \
-H "Accept: unknown/unknown"
.. code-block:: http
HTTP/1.1 415 Unsupported Media Type
{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: unknown/unknown"}
To extend the accepted media types, you can use :ref:`custom_media`.
.. _singular_plural:
@@ -85,6 +114,8 @@ When a singular response is requested but no entries are found, the server respo
Admittedly PostgREST could detect when there is an equality condition holding on all columns constituting the primary key and automatically convert to singular. However this could lead to a surprising change of format that breaks unwary client code just by filtering on an extra column. Instead we allow manually specifying singular vs plural to decouple that choice from the URL format.
.. _stripped_nulls:
Stripped Nulls
--------------
@@ -122,72 +153,6 @@ This returns
{ "id": 13, "name": "Y"}
]
.. _scalar_return_formats:
Scalar Function Response Format
-------------------------------
In the special case of a :ref:`scalar_functions` there are three additional formats:
* ``application/octet-stream``
* ``text/plain``
* ``text/xml``
Example 1: If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header
and select a single column :code:`?select=bin_data`.
.. tabs::
.. code-tab:: http
GET /items?select=bin_data&id=eq.1 HTTP/1.1
Accept: application/octet-stream
.. code-tab:: bash Curl
curl "http://localhost:3000/items?select=bin_data&id=eq.1" \
-H "Accept: application/octet-stream"
Example 2: You can request XML output when having a scalar function that returns a type of ``text/xml``. You are not forced to use select for this case.
.. code-block:: postgres
CREATE FUNCTION generate_xml_content(..) RETURNS xml ..
.. tabs::
.. code-tab:: http
POST /rpc/generate_xml_content HTTP/1.1
Accept: text/xml
.. code-tab:: bash Curl
curl "http://localhost:3000/rpc/generate_xml_content" \
-X POST -H "Accept: text/xml"
Example 3: If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output.
.. code-block:: sql
CREATE FUNCTION get_descriptions(..) RETURNS SETOF TABLE(id int, description text) ..
.. tabs::
.. code-tab:: http
POST /rpc/get_descriptions?select=description HTTP/1.1
Accept: text/plain
.. code-tab:: bash Curl
curl "http://localhost:3000/rpc/get_descriptions?select=description" \
-X POST -H "Accept: text/plain"
.. note::
If more than one row would be returned the binary/plain-text/xml results will be concatenated with no delimiter.
.. _req_body:
Request Body
+4 -2
View File
@@ -311,7 +311,9 @@ Repeating also works in POST requests with ``Content-Type: application/x-www-for
-X POST -H "Content-Type: application/x-www-form-urlencoded" \
-d 'v=1&v=2&v=3&v=4'
Table-Valued functions
.. _table_functions:
Table-Valued Functions
----------------------
A function that returns a table type can be filtered using the same filters as :ref:`tables and views <tables_views>`. They can also use :ref:`Resource Embedding <s_proc_embed>`.
@@ -418,7 +420,7 @@ PostgREST will detect if the function is scalar or table-valued and will shape t
{ "title": "Blade Runner 2049", "rating": 8.1}
]
To manually choose a return format such as binary, plain text or XML, see the section :ref:`scalar_return_formats`.
To manually choose a return format such as binary, see :ref:`custom_media`.
.. _untyped_functions:
-24
View File
@@ -732,30 +732,6 @@ openapi-server-proxy-uri
]
}
.. _raw-media-types:
raw-media-types
---------------
=============== =================================
**Type** String
**Default** `n/a`
**Reloadable** Y
**Environment** PGRST_RAW_MEDIA_TYPES
**In-Database** pgrst.raw_media_types
=============== =================================
This serves to extend the `Media Types <https://en.wikipedia.org/wiki/Media_type>`_ that PostgREST currently accepts through an ``Accept`` header.
These media types can be requested by following the same rules as the ones defined in :ref:`scalar_return_formats`.
As an example, the below config would allow you to request an **image** and a **XML** file by doing a request with ``Accept: image/png``
or ``Accept: font/woff2``, respectively.
.. code:: bash
raw-media-types="image/png, font/woff2"
.. _server_cors_allowed_origins:
server-cors-allowed-origins
-4
View File
@@ -217,10 +217,6 @@ Related to the HTTP request elements.
| | | See :ref:`guc_resp_status`. |
| PGRST112 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst113: | 406 | More than one column was returned for a scalar result. |
| | | See :ref:`scalar_return_formats`. |
| PGRST113 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst114: | 400 | For an :ref:`UPSERT using PUT <upsert_put>`, when |
| | | :ref:`limits and offsets <limits>` are used. |
| PGRST114 | | |
+1 -1
View File
@@ -227,7 +227,7 @@ Notice that the ``response.headers`` should be set to an *array* of single-key o
.. note::
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you also set :ref:`raw-media-types` to something like ``text/html``.
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
.. _guc_resp_status:
-2
View File
@@ -10,7 +10,6 @@ authenticator
backoff
balancer
booleans
Builtins
buildpack
Bytea
Cardano
@@ -175,7 +174,6 @@ url
urlencoded
urls
variadic
vendored
verifier
versioning
Vondra