http_server_core
The http_server_core library provides a portable stream-oriented
server layer on top of the http_core library. It is designed for the
first orchestration step above the wire parser and generator: read
exactly one request from an existing binary stream, dispatch it to a
handler object implementing the http_handler_protocol protocol, and
write exactly one response.
All stream predicates expect binary input and output streams.
This library can be used with backend Prolog systems that support
unbound integer arithmetic and the sockets library: ECLiPSe, SICStus
Prolog, SWI-Prolog, Trealla Prolog, and XVM.
API documentation
Open the ../../apis/library_index.html#http_server_core link in a web browser.
Loading
To load the library, load the loader.lgt file:
| ?- logtalk_load(http_server_core(loader)).
Testing
To test this library, load the tester.lgt file:
| ?- logtalk_load(http_server_core(tester)).
Usage examples
The examples below use in-memory streams or local files so they can be reproduced directly.
Define a simple handler object once:
:- object(notes_http_server_core_echo_handler,
implements(http_handler_protocol)).
handle(Request, Response) :-
http_core::version(Request, Version),
http_core::body(Request, Body),
http_core::response(Version, status(200, 'OK'), [], Body, [], Response).
:- end_object.
To serve a single request from existing binary streams:
| ?- Request = 'POST /echo HTTP/1.1\r\nhost: example.com\r\ncontent-type: text/plain\r\ncontent-length: 5\r\n\r\nhello',
atom_codes(Request, RequestBytes),
open('request.tmp', write, RequestStream, [type(binary)]),
forall(member(RequestByte, RequestBytes), put_byte(RequestStream, RequestByte)),
close(RequestStream),
open('request.tmp', read, Input, [type(binary)]),
open('response.tmp', write, Output, [type(binary)]),
http_server_core::serve(Input, Output, notes_http_server_core_echo_handler),
close(Input),
close(Output),
http_core::parse_response(file('response.tmp'), Response).
Response = response(http(1,1), status(200, 'OK'), _, content('text/plain', text(hello)), _).
To keep serving requests on the same stream pair until persistence rules
say to stop, write several requests into the same input stream and call
serve_connection/3 once:
| ?- Requests = 'POST /echo HTTP/1.1\r\nhost: example.com\r\ncontent-type: text/plain\r\ncontent-length: 3\r\n\r\nonePOST /echo HTTP/1.1\r\nhost: example.com\r\ncontent-type: text/plain\r\ncontent-length: 3\r\n\r\ntwo',
atom_codes(Requests, RequestBytes),
open('requests.tmp', write, RequestStream, [type(binary)]),
forall(member(RequestByte, RequestBytes), put_byte(RequestStream, RequestByte)),
close(RequestStream),
open('requests.tmp', read, Input, [type(binary)]),
open('responses.tmp', write, Output, [type(binary)]),
http_server_core::serve_connection(Input, Output, notes_http_server_core_echo_handler),
close(Input),
close(Output).
The generated responses.tmp file contains two consecutive normalized
HTTP responses, one for one and one for two.
For multipart request inspection, define a handler that uses
http_multipart on the normalized request body:
:- object(notes_http_server_core_multipart_handler,
implements(http_handler_protocol)).
handle(Request, Response) :-
http_core::version(Request, Version),
http_core::body(Request, Body),
http_multipart::fields(Body, [field(title, Title, _FieldParameters)]),
http_multipart::files(Body, [file(upload, Filename, 'text/plain', text(hello), _FileParameters)]),
atomic_list_concat(['title=', Title, '; upload=', Filename], Text),
http_core::response(Version, status(200, 'OK'), [], content('text/plain', text(Text)), [], Response).
:- end_object.
That handler can be used unchanged with serve/3 or
serve_connection/3 because this library exposes multipart requests
in the same normalized form produced by http_core::parse_request/2.
The multipart helper predicates used by handlers return the
parameter-aware field(Name, Value, Parameters) and
file(Name, Filename, MediaType, Payload, Parameters) descriptors
defined by the http_multipart library.
In both descriptor shapes, Parameters is the ordered list of extra
Content-Disposition: form-data parameters. The reserved name and
filename parameters stay explicit helper arguments and are not
repeated in that list.
For a WebSocket opening handshake, define a handler that delegates to
accept_websocket/3:
:- object(notes_http_server_core_websocket_handler,
implements(http_handler_protocol)).
handle(Request, Response) :-
http_server_core::accept_websocket(Request, Response, [protocol(chat)]).
:- end_object.
| ?- Request = 'GET /socket HTTP/1.1\r\nhost: example.com\r\nconnection: Upgrade\r\nupgrade: websocket\r\nsec-websocket-key: dGhlIHNhbXBsZSBub25jZQ==\r\nsec-websocket-version: 13\r\nsec-websocket-protocol: chat\r\n\r\n',
atom_codes(Request, RequestBytes),
open('ws_request.tmp', write, RequestStream, [type(binary)]),
forall(member(RequestByte, RequestBytes), put_byte(RequestStream, RequestByte)),
close(RequestStream),
open('ws_request.tmp', read, Input, [type(binary)]),
open('ws_response.tmp', write, Output, [type(binary)]),
http_server_core::serve_websocket(Input, Output, notes_http_server_core_websocket_handler, Outcome),
close(Input),
close(Output).
Outcome = accepted(RequestTerm, Response),
Response = response(http(1,1), status(101, 'Switching Protocols'), _, empty, _).
After serve_websocket/4 succeeds, higher layers must take ownership
of the underlying upgraded connection or streams. This library stops at
the WebSocket HTTP opening handshake, independently of whether those
streams are plain or TLS-protected.
Current scope
The current implementation provides per-message primitives, a connection loop, and WebSocket handshake helpers:
read_request/2reads exactly one request from a binary stream without requiring end-of-file.write_response/2writes exactly one normalized response to a binary stream.write_response/2streams file-backed response bodies directly after writing the generated header block when the response does not use chunked transfer coding.dispatch/3calls a handler object and normalizes handler failures into500 Internal Server Errorresponses.accept_websocket/3validates a normalized WebSocket opening-handshake request and builds the corresponding101 Switching Protocolsresponse, includingSec-WebSocket-Acceptand optional subprotocol selection.serve_websocket/4reads one request, dispatches one response, writes it to the output stream, and reports whether the exchange completed with a valid WebSocket opening-handshake response.serve/3connects request reading, handler dispatch, and response writing, returning400 Bad Requestfor malformed input.serve_connection/3repeatedly serves requests on the same stream pair using HTTP persistence rules and stops on end-of-file orConnection: closesemantics.serve/3andserve_connection/3send the generated response headers forHEADrequests but suppress the response body bytes, including for file-backed responses.
Because incoming request bodies are normalized by the http_core
library, handler objects can inspect multipart/form-data requests
directly using the http_multipart predicates after reading or
serving a request.
QUERY requests are read and dispatched like any other normalized
request. A resource handler that implements RFC 10008 QUERY semantics
remains responsible for rejecting missing or unsupported query content
metadata, such as absent or unsupported Content-Type, with the
appropriate 4xx response.
This layer intentionally stays transport-neutral. It does not accept sockets, listen for connections, or manage concurrency. Those concerns belong to higher layers or backend-specific integration built on top of this stream contract.
Framing rules
Request framing currently supports:
header termination using
CRLF CRLFfixed-length bodies via
Content-Lengthchunked request bodies via
Transfer-Encoding: chunked, including trailers
When neither Content-Length nor Transfer-Encoding is present,
the request is read as having an empty body so that subsequent bytes
remain available for the next request on the stream.
Multipart workflow
This library does not introduce a separate multipart server API.
Multipart requests are exposed to handlers as the same normalized
request bodies produced by http_core::parse_request/2, which means
handlers can use http_multipart::fields/2 and
http_multipart::files/2 directly on the request body.
WebSocket handshake workflow
This layer now provides a focused helper for the HTTP opening handshake:
Handlers can call
accept_websocket/3to validate an incoming normalized handshake request and build the matching101response.Servers that need to stop after one opening handshake can call
serve_websocket/4and inspect whether it returnedaccepted(Request, Response)orrejected(Response).The optional
protocol(Protocol)option selects one offered subprotocol.The helper is intentionally transport-neutral: it handles the WebSocket HTTP opening handshake on existing binary streams, independently of whether those streams are plain or TLS-protected.
It does not take ownership of the upgraded stream or begin frame processing. Higher layers that need to continue on the upgraded connection must stop at a single served message and then take over the underlying stream or socket.
Upgraded connection handles and frame/session loops are provided by higher layers such as
http_socket_transport,http_process_transport, andhttp_websocket_service.The
http_websocket_server_service::serve_once/6-7predicates build on top of this handshake helper when server-side code wants a single entry point for handshake plus callback-driven session execution.
Current limitations
Only the transport coding sequence
[chunked]is recognized when reading streamed request bodies.The library does not provide socket accept loops, connection pooling, or concurrency management.