These conventions should be followed by connector implementations to make it easier for users to swap them without fuss.
The conventions may evolve in the future to adapt to new connector architectures.
Service connectors should run services defined as a service function of the following type:
Http.Request.t -> Http.Response.t
The duty of the connector is to receive requests from an interface, construct suitable Webs.Http.Request.t
value for them, call them on the service function to get a Webs.Http.Response.t
value to send back on the interface.
Service functions are not expected to raise exceptions. If a service function is unable to fulfill its duty it should error with a suitable server error HTTP response. However connectors should be prepared to catch any unexpected exception raised from the service function and log it somewhere, e.g. via Webs.Http.Connector.Log.msg
.
The suggested base signature of connectors is:
open Webs
type t
(** The type for service connectors. *)
val make :
?log:(Http.Connector.Log.msg -> unit) ->
?service_path:Http.Path.t -> unit -> t
(** [make ()] is a new service connector. *)
val serve : t -> (Http.Request.t -> Http.Response.t) -> (unit, string) result
(** [serve c service] serves one or more requests with [service]. *)
A service connector should also properly document how it handles parallelism and concurrency whenever it calls the service function.
service_path
property to indicate the root path on which the service is attached. This is used to derive the Webs.Http.Request.path
value of requests and makes it easier to relocate services. See for example Webs_http11_gateway.service_path
.Webs.Http.Connector.Default
and support a Webs.Http.Connector.Log.msg
log. The Webs_http11_gateway
connector can be used as a blueprint.The request value constructed by the connector should satisfy the following constraints.
Webs.Http.Request.raw_path
should be the request target (HTTP/1.1) or :path
pseudo-header (HTTP/2, HTTP/3) of the request.Webs.Http.Request.service_path
should be the service path of the connector. This indicates the root path on which the service is attached.Webs.Http.Request.path
should be the decoded path of Webs.Http.Request.raw_path
stripped by the Webs.Http.Request.service_path
. If this operation fails, the connector should respond with a Webs.Http.Status.bad_gateway_502
and not call the service since this indicates a misconfiguration in the service setup (e.g. the gateway and the service disagree on the service path).Webs.Http.Request.query
should represent the raw query of Webs.Http.Request.raw_path
(if any).Webs.Http.Request.version
not very important in practice but it should be the HTTP version of the request made on the connector. If the connector interfaces with a gateway this may be different from the actual version used by the gateway with the client.Webs.Http.Request.headers
and Webs.Http.Request.body
constraints:
Webs.Http.Headers.content_type
. If such a header exists in Webs.Http.Request.headers
, it must be added to the request's Webs.Http.Body.t
value.Webs.Http.Headers.content_length
. If such a header exists in Webs.Http.Request.headers
, it must be added to request's Webs.Http.Body.t
value.Webs.Http.Headers.host
, for HTTP/2 and HTTP/3 connectors, if no such header is present in the request, it should add them with the value of the :authority
pseudo-header value.If the connector fails to create a request value to give to the service function it should respond itself according to the connector response conventions.
Except for the point 6.3, the function Webs.Http.Request.for_service_connector
constructs a request value that satisfies these constraints.
When sending the service response, connectors should treat these headers of the response specially:
Webs.Http.Headers.content_type
. If such a header exists in Webs.Http.Response.headers
, this value must be written in the response. Otherwise the value found in Webs.Http.Body.content_type
must be used. In both cases if the value found is Webs.Media_type.none
, no header should be output.Webs.Http.Headers.content_length
, if such a header exists in Webs.Http.Response.headers
, this value must be written in the response. Otherwise the value found in Webs.Http.Body.content_length
must be used (if any). If the value found is ""
or None
, no header should be output.The function Webs.Http.Headers.for_connector
returns a Webs.Http.Headers.t
value for which that logic has been performed.
Note that in general it's better if responses constructed by service functions let the content type and length be defined by bodies: it's less error prone when the responses get massaged with Webs.Http.Response.with_body
.
Connectors may need to respond to a request without passing them to the service function. For example because they are unable to parse the request into a request value. When they do, they should do so as follows:
Webs.Http.Status.bad_request_400
.Webs.Http.Request.path
value it should respond with Webs.Http.Status.bad_gateway_502
. This indicates a misconfiguration in the service setup (e.g. the gateway and the connector disagree on the service path).Webs.Http.Status.content_too_large_413
. Example of limits are Webs.Http.Connector.Default.max_request_headers_byte_size
and Webs.Http.Connector.Default.max_request_body_byte_size
.Webs.Http.Status.server_error_500
.For HTTP/1.1 connectors, if a Webs.Http.Headers.expect
header with 100-continue
is found in the request then it should handle and hide the Expect
response dance (see other explanations). This can be done by either:
Webs.Http.Status.continue_100
once the service function starts reading the request body. For the sake of transparency, the header should still be as kept as is in the request value headers. This allows services functions to know the connector did that. The Webs_http11_gateway
does that.Webs.Http.Status.expectation_failed_417
without invoking the service function. A well behaved client should try again without the header. The Webs_cgi
connector does that.Note that if your connector interacts with a gateway you may well never see that header. For example it seems nginx will automatically send a 100-continue response itself. Finally this issue is irrelevant for HTTP/2 and HTTP/3.
It is advised for client connectors to implement the Webs.Http_client.T
signature.
The scheme of the request must be derived from Webs.Http.Request.scheme
. The server and port to connect to should be derived from the client request value by looking up the Webs.Http.Headers.host
header and the request Webs.Http.Request.scheme
. If no port is found a default one should be derived from the scheme. The function Webs.Http.Headers.decode_host
performs that logic.
When writing the client request, connectors should treat these headers of the request specially:
Webs.Http.Headers.content_type
. If such a header exists in Webs.Http.Request.headers
, this value must be written in the request. Otherwise the value found in Webs.Http.Body.content_type
must be used. In both cases if the value found is Webs.Media_type.none
, no header should be output.Webs.Http.Headers.content_length
, if such a header exists in Webs.Http.Request.headers
, this value must be written in the request. Otherwise the value found in Webs.Http.Body.content_length
must be used (if any). If the value found is ""
or None
, no header should be output.The function Webs.Http.Headers.for_connector
returns a Webs.Http.Headers.t
value for which that logic has been performed.
Note that in general it's better if one lets the content type and length be defined by bodies: it's less error prone if the request body gets changed by them with Webs.Http.Response.with_body
.
The response value constructed by the connector should satisfy the following constraints:
Webs.Http.Response.headers
and Webs.Http.Response.body
constraints. The Webs.Http.Headers.content_type
and Webs.Http.Headers.content_length
should not be part of the response headers they should only be added on the response body value.