Library
Module
Module type
Parameter
Class
Class type
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_string (Ok ("hello " ^name ^"!\n")));
(* echo request *)
S.add_route_handler server
S.Route.(exact "echo" @/ return)
(fun req -> S.Response.make_string
(Ok (Format.asprintf "echo:@ %a@." S.Request.pp req)));
(* file upload *)
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)
);
(* run the server *)
Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server);
match S.run server with
| Ok () -> ()
| Error e -> raise e
It 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"}
These buffers are used to avoid allocating too many byte arrays when processing streams and parsing requests.
module Buf_ : sig ... end
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 = {
bs_fill_buf : unit -> bytes * int * int;
See the current slice of the internal buffer as bytes, i, len
, where the slice is bytes[i] .. [bytes[i+len-1]]
. Can block to refill the buffer if there is currently no content. If len=0
then there is no more data.
bs_consume : int -> unit;
Consume n bytes from the buffer. This should only be called with n <= len
after a call to is_fill_buf
that returns a slice of length len
.
bs_close : unit -> unit;
Close the stream.
*)}
A buffered stream, with a view into the current buffer (or refill if empty), and a function to consume n
bytes. See Byte_stream
for more details.
module Byte_stream : sig ... end
module Meth : sig ... end
Headers are metadata associated with a request or response.
module Headers : sig ... end
Requests are sent by a client, e.g. a web browser or cURL.
module Request : sig ... end
module Response_code : sig ... end
Responses are what a http server, such as Tiny_httpd
, send back to the client to answer a Request.t
module Response : sig ... end
module Route : sig ... end
A HTTP server. See create
for more details.
val create :
?masksigpipe:bool ->
?max_connections:int ->
?new_thread:((unit -> unit) -> unit) ->
?addr:string ->
?port:int ->
?sock:Unix.file_descr ->
unit ->
t
Create a new webserver.
The server will not do anything until run
is called on it. Before starting the server, one can use add_path_handler
and set_top_handler
to specify how to handle incoming requests.
val addr : t -> string
Address on which the server listens.
val is_ipv6 : t -> bool
is_ipv6 server
returns true
iff the address of the server is an IPv6 address.
val port : t -> int
Port on which the server listens.
val add_decode_request_cb :
t ->
(unit Request.t -> (unit Request.t * (byte_stream -> byte_stream)) option) ->
unit
Add 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-Encoding
header and returning a stream transformer that decompresses on the fly.
val add_encode_response_cb :
t ->
(unit Request.t -> Response.t -> Response.t option) ->
unit
Add a callback for every request/response pair. Similarly to add_encode_response_cb
the 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) -> unit
Setup 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 a 404
not found.
val add_route_handler :
?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
?meth:Meth.t ->
t ->
('a, string Request.t -> Response.t) Route.t ->
'a ->
unit
add_route_handler server Route.(exact "path" @/ string @/ int @/ return) f
calls f "foo" 42 request
when a request
with path "path/foo/42/" is received.
Note that the handlers are called in the reverse order of their addition, so the last registered handler can override previously registered ones.
val add_route_handler_stream :
?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
?meth:Meth.t ->
t ->
('a, byte_stream Request.t -> Response.t) Route.t ->
'a ->
unit
Similar 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 as Jsonm
) or into a file.
val add_path_handler :
?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
?meth:Meth.t ->
t ->
('a,
Scanf.Scanning.in_channel,
'b,
'c ->
string Request.t ->
Response.t,
'a ->
'd,
'd)
format6 ->
'c ->
unit
Similar to add_route_handler
but based on scanf.
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.
val add_path_handler_stream :
?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
?meth:Meth.t ->
t ->
('a,
Scanf.Scanning.in_channel,
'b,
'c ->
byte_stream Request.t ->
Response.t,
'a ->
'd,
'd)
format6 ->
'c ->
unit
Similar 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 as Jsonm
) or into a file.
EXPERIMENTAL: this API is not stable yet.
module type SERVER_SENT_GENERATOR = sig ... end
A server-side function to generate of Server-sent events.
type server_sent_generator = (module SERVER_SENT_GENERATOR)
Server-sent event generator
val add_route_server_sent_handler :
?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
t ->
('a, string Request.t -> server_sent_generator -> unit) Route.t ->
'a ->
unit
Add a handler on an endpoint, that serves server-sent events.
The callback is given a generator that can be used to send events as it pleases. The connection is always closed by the client, and the accepted method is always GET
. This will set the header "content-type" to "text/event-stream" automatically and reply with a 200 immediately. See server_sent_generator
for more details.
This handler stays on the original thread (it is synchronous).