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/2 reads exactly one request from a binary stream without requiring end-of-file.

  • write_response/2 writes exactly one normalized response to a binary stream.

  • write_response/2 streams file-backed response bodies directly after writing the generated header block when the response does not use chunked transfer coding.

  • dispatch/3 calls a handler object and normalizes handler failures into 500 Internal Server Error responses.

  • accept_websocket/3 validates a normalized WebSocket opening-handshake request and builds the corresponding 101 Switching Protocols response, including Sec-WebSocket-Accept and optional subprotocol selection.

  • serve_websocket/4 reads 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/3 connects request reading, handler dispatch, and response writing, returning 400 Bad Request for malformed input.

  • serve_connection/3 repeatedly serves requests on the same stream pair using HTTP persistence rules and stops on end-of-file or Connection: close semantics.

  • serve/3 and serve_connection/3 send the generated response headers for HEAD requests 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 CRLF

  • fixed-length bodies via Content-Length

  • chunked 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/3 to validate an incoming normalized handshake request and build the matching 101 response.

  • Servers that need to stop after one opening handshake can call serve_websocket/4 and inspect whether it returned accepted(Request, Response) or rejected(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, and http_websocket_service.

  • The http_websocket_server_service::serve_once/6-7 predicates 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.