________________________________________________________________________

This file is part of Logtalk <https://logtalk.org/>  
SPDX-FileCopyrightText: 1998-2026 Paulo Moura <pmoura@logtalk.org>  
SPDX-License-Identifier: Apache-2.0

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
________________________________________________________________________


`http_client`
=============

The `http_client` library provides a request-oriented client layer on top of the
`http_core`, `url`, and HTTP transport libraries. It is the request-oriented
client-side entry point: it builds normalized requests from absolute URLs plus
options and delegates transport to the selected
`http_transport_protocol`-compatible layer, such as `http_socket_transport` or
`http_process_transport`.

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.

By default, `transport(default)` selects `http_socket_transport` for `http://`
and `ws://` URLs, and `http_process_transport` for `https://` and `wss://`
URLs. Applications can select a transport explicitly with the `transport/1`
option when they need lower-level control or a custom transport object.

When the selected transport supports secure schemes, the request-oriented
facade defaults `https://` and `wss://` URLs without an explicit port to
port `443` and adds `connection_transport(tls)` to the connection options
automatically unless already specified explicitly. This TLS-aware behavior
applies notably to `http_process_transport`, which provides transport support
for secure schemes via the same `http_transport_protocol` interface.


API documentation
-----------------

Open the [../../apis/library_index.html#http_client](../../apis/library_index.html#http_client)
link in a web browser.


Loading
-------

To load the library, load the `loader.lgt` file:

	| ?- logtalk_load(http_client(loader)).


Testing
-------

To test this library, load the `tester.lgt` file:

	| ?- logtalk_load(http_client(tester)).


Usage examples
--------------

The examples below are self-contained. They use the high-level `http_server`
facade to start a local loopback server, so they can be reproduced without any
external service. They assume that both `http_client(loader)` and
`http_server(loader)` are loaded and that the backend supports threads, as the
`http_server::start/4-5` and `http_server::stop/1` helpers run the local server
concurrently with the client call.

Define a small echo handler once:

	:- object(notes_http_client_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.

Start a local server, send a request, and inspect the normalized response. The
unbound `Port` argument is unified with the actual port selected by the backend:

	| ?- http_server::start('127.0.0.1', Port, notes_http_client_echo_handler, Server, []),
	     atomic_list_concat(['http://127.0.0.1:', Port, '/echo'], URL),
	     http_client::post(URL, content('text/plain', text(hello)), Response, []),
	     http_server::stop(Server).
	
	Response = response(http(1,1), status(200, 'OK'), _, content('text/plain', text(hello)), _).

The QUERY method uses the same body-carrying convenience shape as POST, PUT,
and PATCH:

	| ?- http_server::start('127.0.0.1', Port, notes_http_client_echo_handler, Server, []),
	     atomic_list_concat(['http://127.0.0.1:', Port, '/contacts'], URL),
	     http_client::query(URL, content('application/x-www-form-urlencoded', form([limit-'10'])), Response, []),
	     http_server::stop(Server).
	
	Response = response(http(1,1), status(200, 'OK'), _, content('application/x-www-form-urlencoded', form([limit-'10'])), _).

For a multipart form-data request, define a handler that inspects the normalized
request body using `http_multipart`:

	:- object(notes_http_client_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.

An example form-data request can then be:

	| ?- http_server::start('127.0.0.1', Port, notes_http_client_multipart_handler, Server, []),
	     atomic_list_concat(['http://127.0.0.1:', Port, '/upload'], URL),
	     http_client::post(
	         URL,
	         form_data([
	             field(title, 'Logtalk', []),
	             file(upload, 'notes.txt', 'text/plain', text(hello), [])
	         ]),
	         Response,
	         []
	     ),
	     http_server::stop(Server).
	
	Response = response(http(1,1), status(200, 'OK'), _, content('text/plain', text('title=Logtalk; upload=notes.txt')), _).

For a WebSocket opening handshake, define a handler that accepts the upgrade:

	:- object(notes_http_client_websocket_handler,
		implements(http_handler_protocol)).
		
		handle(Request, Response) :-
			http_server_core::accept_websocket(Request, Response, [protocol(chat)]).
	
	:- end_object.

An example opening handshake query can then be:

	| ?- http_server::start('127.0.0.1', Port, notes_http_client_websocket_handler, Server, []),
	     atomic_list_concat(['ws://127.0.0.1:', Port, '/socket'], URL),
	     http_client::open_websocket(URL, Connection, Response, [protocols([chat]), key('dGhlIHNhbXBsZSBub25jZQ==')]),
	     http_socket_transport::close_connection(Connection),
	     http_server::stop(Server).
	
	Response = response(http(1,1), status(101, 'Switching Protocols'), _, empty, _).

The `Connection` term returned by `open_websocket/4` remains open. After the
handshake, either close it explicitly with `http_socket_transport::close_connection/1`
or hand it to the `http_websocket`, `http_websocket_messages`,
`http_websocket_session`, or `http_websocket_service` libraries.


Current scope
-------------

The initial request-oriented implementation provides:

- `request/4` for one-shot requests against absolute URLs supported by the
	selected transport.
- `open_websocket/4` for validated WebSocket opening handshakes against
	absolute WebSocket URLs supported by the selected transport, returning the
	upgraded reusable connection handle and the `101` response.
- `request/5` for requests over an already open compatible reusable
	connection or connection pool handle, with endpoint validation against the
	supplied URL.
- `get/3-4`, `head/3-4`, `delete/3-4`, `post/4-5`, `put/4-5`, and `patch/4-5`
  convenience predicates.

When the same request-construction or WebSocket opening-handshake option is
given multiple times, the first occurrence is used.

Supported request options are:

- `transport(Transport)` to select the transport object. Default: `default`,
  which selects the transport from the URL scheme.
- `headers(Headers)` to supply normalized request headers.
- `body(Body)` to supply a normalized request body. The request-oriented
  facade also accepts `body(form_data(Items))` as a convenience descriptor for
  multipart form-data requests; in that case it builds the multipart body via
  `http_multipart` and injects a generated boundary property unless the caller
  already provided one in `properties/1`.
- `query(Pairs)` to append URL-encoded query pairs to the URL query string.
- `version(Version)` to override the default `http(1,1)` version.
- `properties(Properties)` to supply additional normalized request properties.
- `connection_options(Options)` to pass transport-specific options through to
	the underlying `open_connection/4` call for one-shot requests. When the URL
	uses the `https://` scheme, `connection_transport(tls)` is added
	automatically unless `Options` already specify a transport explicitly.

Supported WebSocket opening-handshake options are:

- `transport(Transport)` to select the transport object. Default: `default`,
  which selects the transport from the URL scheme.
- `headers(Headers)` to supply additional normalized handshake request headers
  other than the handshake-managed headers.
- `query(Pairs)` to append URL-encoded query pairs to the URL query string.
- `version(Version)` to override the default `http(1,1)` version. The opening
  handshake requires HTTP/1.1 or later.
- `protocols(Protocols)` to request one or more subprotocol tokens.
- `key(Key)` to provide an explicit `Sec-WebSocket-Key` value. When omitted, a
  fresh key is generated automatically.
- `connection_options(Options)` to pass transport-specific options through to
	the selected transport `open_connection/4` predicate. When the URL uses the
	`wss://` scheme, `connection_transport(tls)` is added automatically unless
	`Options` already specify a transport explicitly.

The stream-based primitives remain available from the `http_client_core`
object.

Multipart workflow
------------------

The current multipart workflow is intentionally small but practical:

- For generic multipart bodies, callers can keep using normalized
  `content(MediaType, multipart(Parts))` terms.
- For common form-data requests, callers can use `form_data(Items)` through the
  existing body path, including `post/4-5`, `put/4-5`, and `patch/4-5`.
- The `Items` descriptors are the same `field(Name, Value, Parameters)` and
  `file(Name, Filename, MediaType, Payload, Parameters)` descriptors
  supported by the `http_multipart::form_data_body/2` helper.
- In both descriptor shapes, `Parameters` is the ordered list of extra
  `Content-Disposition: form-data` parameters to preserve or generate.
- The reserved `name` and `filename` parameters stay explicit helper
  arguments and must not be repeated in the `Parameters` list.
- When using the convenience descriptor, the client facade adds a multipart
  boundary property automatically so the request can be generated on the wire
  without extra caller bookkeeping.

For example, callers can send extra disposition parameters explicitly:

	| ?- http_client::post(
	     URL,
	     form_data([
	         field(title, 'Logtalk', [charset-utf8]),
	         file(upload, 'notes.txt', 'text/plain', text(hello), [creation_date-'2026-06-08'])
	     ]),
	     Response,
	     []
	 ).

WebSocket workflow
------------------

The current WebSocket workflow is intentionally limited to the opening
handshake:

- Call `open_websocket/4` with an absolute WebSocket URL supported by the
	selected transport.
- The helper opens a dedicated reusable transport connection, sends a
  normalized opening-handshake request, and validates the server `101`
  response including the `Sec-WebSocket-Accept` value.
- When the call succeeds, the returned connection handle remains open so a
  higher layer can take over the upgraded stream, typically by calling
	the selected transport `connection_streams/3` predicate and then using the `http_websocket`
  frame predicates, the `http_websocket_messages` message predicates, or the
  stateful `http_websocket_session` predicates with explicit close-state and
  automatic control-message handling, including the higher-level
	`http_websocket_service` `run_session/3-4` callback loop.
- When client-side code wants one entry point for handshake plus session
	execution, the `http_websocket_client_service::open/4-5` predicates layer
	on top of `open_websocket/4`, can write optional initial outbound messages,
	and then run the callback-driven session loop.
- Callers are responsible for eventually closing that connection using
	the selected transport `close_connection/1` predicate if a later layer does
	not take ownership of it. The `http_websocket_service::run_session/3-4`
	predicates do take
  ownership of the upgraded connection and close it automatically when the
	session loop finishes, as do the higher-level
	`http_websocket_client_service::open/4-5` predicates.


Current limitations
-------------------

- The `open_websocket/4` predicate is limited to opening-handshake validation.
  See the dedicated WebSocket libraries for frame parsing, message reassembly,
  and related functionality.
