Intro¶
omni_httpd is an embedded HTTP server for Postgres. It allows one to write HTTP request handling in SQL. This approach lends to less indirection that is common in traditional application architectures, where applications are interacting with the database directly.
Getting started.¶
By default, omni_httpd
, once installed, will provide a default page on port 8080
.
You can change the handler by updating the query
column in the handlers
table. Currently, the idiomatic way
to write this query is to use omni_httpd.cascading_query
aggregate function (though, of course, one can roll their
own SQL completely by hand). This function simplifies building priority-sorted request handling:
update omni_httpd.handlers
set
query =
(select
omni_httpd.cascading_query(name, query order by priority desc nulls last)
from
(values
('headers',
$$select omni_httpd.http_response(body => request.headers::text) from request where request.path = '/headers'$$,
1),
('not_found',
$$select omni_httpd.http_response(status => 404, body => 'Not found') from request$$, 0))
as routes(name, query, priority));
What if the query is invalid?
omni_httpd enforces validity of the query using a constraint trigger at the transaction boundary when updating or inserting a handler. This means that once the transaction is being committed, the query is validated and if it, say, refers to an unknown relation, column or is invalid for other reasons, it will be rejected and the transaction will not succeed.
Please note, however, that at this moment, this enforcement will not help avoiding runtime errors if you render your query invalid afterwards (for example, by dropping relations it references), this will lead to runtime errors, ultimately leading to HTTP 500 responses.
The query called headers
will dump request's headers, not_found
will return HTTP 404. We can test it with curl
:
$ curl http://localhost:8080
Not found
$ curl http://localhost:8080/headers
{"(user-agent,curl/7.86.0,t)","(accept,*/*,t)"}
The above method of defining the handler can work well when the queries that it is composed of are either stored in a database or can be retrieved during deployment (say, from a Git repository or any other source.)
What did you mean by "priority-sorted" request handling?
If you look at the order of handlers we added (headers
followed by
not_found
), it means that cascading_query
-built query will first try to get results from headers
and if none available, will
attempt not_found
. Suppose we changed the order:
update omni_httpd.handlers
set
query =
(select
omni_httpd.cascading_query(name, query order by priority asc nulls last) -- (1)
from
(values
('headers',
$$select omni_httpd.http_response(body => request.headers::text) from request where request.path = '/headers'$$,
1),
('not_found',
$$select omni_httpd.http_response(status => 404, body => 'Not found') from request$$, 0))
as routes(name, query, priority));
- We changed the order from
desc
toasc
Then not_found
will always take the precedence:
Tip
An interesting corollary to this approach is that if all of the handling sub-queries are of the same priority,
then priority-ordering is not required and one can simply use cascading_query
without ORDER BY
.
What does cascading_query
do?
The idea behind cascading_query
is that it aggregates named queries in a UNION
query where all given queries
will become common table expressions (CTEs) and the UNION
will be used to cascade over them, something like:
All good. But looking back into the queries itself, they mention request
which is nowhere to be found. Where does this
come from? This is actually a CTE that omni_httpd
supplies in runtime that has the following omni_httpd.http_request
signature:
method omni_http.http_method,
path text,
query_string text,
body bytea,
headers omni_http.http_header[]
Tip
If this signature seem a little incomplete (where's the source IP address, can the body by streamed, etc.?), that's because it is still work in progress. Please consider contributing if you feel up to it.
Also, omni_web provides complementary higher-level functionality.
In order to test your request handlers without having to run actual HTTP
requests against it, one can use omni_httpd.http_request
function to compose
requests:
with
request as (select (omni_httpd.http_request('/')).*)
select
omni_httpd.http_response(request.path)
from
request
Parameter | Type | Description | Default |
---|---|---|---|
path | text | Path | None |
method | omni_http.http_method | HTTP method [^http-method] | GET |
query_string | text | Query string | NULL |
headers | omni_http.http_headers | An array of HTTP headers | empty array |
body | bytea | Request body | NULL |
Configuration¶
omni_httpd
can be configured with the following PostgreSQL configuration variables:
omni_httpd.http_workers
to configure the number of http workers. It defaults to the number of cpus and adjusts if this is higher than what max_worker_processes allows.omni_httpd.temp_dir
to set the temporary directory foromni_httpd
files like unix domain sockets, defaults to/tmp
Controls¶
To stop or start omni_httpd server, one can use omni_httpd.stop()
and omni_httpd.start()
procedures. They both
accept a boolean immediate
parameter (defaults to false
), which
will perform the operation immediately without waiting for the transaction to successfully commit.
When is it useful?
Beyond the most obvious scenario, in which you just need the server to be stopped, there's a case when the database it is running in requires to be free of users. For example, if you want to create another database with the primary database as its template (for example, for isolated testing).
Bear in mind that currently, in order for the templated database to get its HTTP server running, you need to connect to it. This may change in the future.