Module Tiny_httpd
Tiny Http Server
This library implements a very simple, basic HTTP/1.1 server using blocking IOs and threads. Basic routing based on Scanf is provided for convenience, so that several handlers can be registered.
It is possible to use a thread pool, see create's argument new_thread.
The echo example (see src/examples/echo.ml) demonstrates some of the features by declaring a few endpoints, including one for uploading files:
module S = Tiny_httpd
let () =
let server = S.create () in
(* say hello *)
S.add_route_handler ~meth:`GET server
S.Route.(exact "hello" @/ string @/ return)
(fun name _req -> S.Response.make_ok ("hello " ^name ^"!\n"));
(* echo request *)
S.add_route_handler server
S.Route.(exact "echo" @/ return)
(fun req -> S.Response.make_ok (Format.asprintf "echo:@ %a@." S.Request.pp req));
S.add_route_handler ~meth:`PUT server
S.Route.(exact "upload" @/ string_urlencoded @/ return)
(fun path req ->
try
let oc = open_out @@ "/tmp/" ^ path in
output_string oc req.S.Request.body;
flush oc;
S.Response.make_string (Ok "uploaded file")
with e ->
S.Response.fail ~code:500 "couldn't upload file: %s"
(Printexc.to_string e)
);
Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server);
match S.run server with
| Ok () -> ()
| Error e -> raise eIt is then possible to query it using curl:
$ dune exec src/examples/echo.exe &
listening on http://127.0.0.1:8080
# the path "hello/name" greets you.
$ curl -X GET http://localhost:8080/hello/quadrarotaphile
hello quadrarotaphile!
# the path "echo" just prints the request.
$ curl -X GET http://localhost:8080/echo --data "howdy y'all"
echo:
{meth=GET;
headers=Host: localhost:8080
User-Agent: curl/7.66.0
Accept: */*
Content-Length: 10
Content-Type: application/x-www-form-urlencoded;
path="/echo"; body="howdy y'all"}Tiny buffer implementation
These buffers are used to avoid allocating too many byte arrays when processing streams and parsing requests.
module Buf_ : sig ... endGeneric stream of data
Streams are used to represent a series of bytes that can arrive progressively. For example, an uploaded file will be sent as a series of chunks.
type byte_stream={}A buffered stream, with a view into the current buffer (or refill if empty), and a function to consume
nbytes. SeeByte_streamfor more details.
module Byte_stream : sig ... endMethods
module Meth : sig ... endHeaders
Headers are metadata associated with a request or response.
module Headers : sig ... endRequests
Requests are sent by a client, e.g. a web browser or cURL.
module Request : sig ... endResponse Codes
module Response_code : sig ... endResponses
Responses are what a http server, such as Tiny_httpd, send back to the client to answer a Request.t
module Response : sig ... endmodule Route : sig ... end
Server
type tA HTTP server. See
createfor more details.
val create : ?masksigpipe:bool -> ?max_connections:int -> ?new_thread:((unit -> unit) -> unit) -> ?addr:string -> ?port:int -> unit -> tCreate a new webserver.
The server will not do anything until
runis called on it. Before starting the server, one can useadd_path_handlerandset_top_handlerto specify how to handle incoming requests.- parameter masksigpipe
if true, block the signal
Sys.sigpipe which otherwise tends to kill client threads when they try to write on broken sockets. Default:true.
- parameter new_thread
a function used to spawn a new thread to handle a new client connection. By default it is
Thread.create but one could use a thread pool instead.
- parameter max_connections
maximum number of simultaneous connections.
- parameter addr
address (IPv4 or IPv6) to listen on. Default
"127.0.0.1".
- parameter port
to listen on. Default
8080.
val addr : t -> stringAddress on which the server listens.
val is_ipv6 : t -> boolis_ipv6 serverreturnstrueiff the address of the server is an IPv6 address.- since
- 0.3
val port : t -> intPort on which the server listens.
val add_decode_request_cb : t -> (unit Request.t -> (unit Request.t * (byte_stream -> byte_stream)) option) -> unitAdd a callback for every request. The callback can provide a stream transformer and a new request (with modified headers, typically). A possible use is to handle decompression by looking for a
Transfer-Encodingheader and returning a stream transformer that decompresses on the fly.
val add_encode_response_cb : t -> (unit Request.t -> Response.t -> Response.t option) -> unitAdd a callback for every request/response pair. Similarly to
add_encode_response_cbthe callback can return a new response, for example to compress it. The callback is given the query with only its headers, as well as the current response.
val set_top_handler : t -> (string Request.t -> Response.t) -> unitSetup a handler called by default.
This handler is called with any request not accepted by any handler installed via
add_path_handler. If no top handler is installed, unhandled paths will return a404not found.
val add_path_handler : ?accept:(unit Request.t -> (unit, Response_code.t * string) Stdlib.result) -> ?meth:Meth.t -> t -> ('a, Stdlib.Scanf.Scanning.in_channel, 'b, 'c -> string Request.t -> Response.t, 'a -> 'd, 'd) Stdlib.format6 -> 'c -> unitadd_path_handler server "/some/path/%s@/%d/" fcallsf "foo" 42 requestwhen a request with path "some/path/foo/42/" is received.This uses
Scanf's splitting, which has some gotchas (in particular,"%s"is eager, so it's generally necessary to delimit its scope with a"@/"delimiter. The "@" before a character indicates it's a separator.Note that the handlers are called in the reverse order of their addition, so the last registered handler can override previously registered ones.
- parameter meth
if provided, only accept requests with the given method. Typically one could react to
`GETor`PUT.
- parameter accept
should return
Ok()if the given request (before its body is read) should be accepted,Error (code,message)if it's to be rejected (e.g. because its content is too big, or for some permission error). See thehttp_of_dirprogram for an example of how to useacceptto filter uploads that are too large before the upload even starts.
val add_route_handler : ?accept:(unit Request.t -> (unit, Response_code.t * string) Stdlib.result) -> ?meth:Meth.t -> t -> ('a, string Request.t -> Response.t) Route.t -> 'a -> unitval add_path_handler_stream : ?accept:(unit Request.t -> (unit, Response_code.t * string) Stdlib.result) -> ?meth:Meth.t -> t -> ('a, Stdlib.Scanf.Scanning.in_channel, 'b, 'c -> byte_stream Request.t -> Response.t, 'a -> 'd, 'd) Stdlib.format6 -> 'c -> unitSimilar to
add_path_handler, but where the body of the request is a stream of bytes that has not been read yet. This is useful when one wants to stream the body directly into a parser, json decoder (such asJsonm) or into a file.- since
- 0.3
val add_route_handler_stream : ?accept:(unit Request.t -> (unit, Response_code.t * string) Stdlib.result) -> ?meth:Meth.t -> t -> ('a, byte_stream Request.t -> Response.t) Route.t -> 'a -> unitSimilar to
add_route_handler, but where the body of the request is a stream of bytes that has not been read yet. This is useful when one wants to stream the body directly into a parser, json decoder (such asJsonm) or into a file.- since
- 0.6
val stop : t -> unitAsk the server to stop. This might not have an immediate effect as
runmight currently be waiting on IO.
val run : t -> (unit, exn) Stdlib.resultRun the main loop of the server, listening on a socket described at the server's creation time, using
new_threadto start a thread for each new client.This returns
Ok ()if the server exits gracefully, orError eif it exits with an error.