diff --git a/README.md b/README.md index 27e8968d..0dbc50ff 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,32 @@ it allows downloading the files, and listing directories. If a directory contains `index.html` then this will be served instead of listing the content. +## Steaming response body + +Tiny_httpd provides multiple ways of returning a body in a response. +The response body type is: + +```ocaml +type body = + [ `String of string + | `Stream of byte_stream + | `Writer of Tiny_httpd_io.Writer.t + | `Void ] +``` + +The simplest way is to return, say, `` `String "hello" ``. The response +will have a set content-length header and its body is just the string. +Some responses don't have a body at all, which is where `` `Void `` is useful. + +The `` `Stream _ `` case is more advanced and really only intended for experts. + +The `` `Writer w `` is new, and is intended as an easy way to write the +body in a streaming fashion. See 'examples/writer.ml' to see a full example. +Typically the idea is to create the body with `Tiny_httpd_io.Writer.make ~write ()` +where `write` will be called with an output channel (the connection to the client), +and can write whatever it wants to this channel. Once the `write` function returns +the body has been fully sent and the next request can be processed. + ## Socket activation Since version 0.10, socket activation is supported indirectly, by allowing a diff --git a/examples/dune b/examples/dune index 4cafb199..dd8e19a3 100644 --- a/examples/dune +++ b/examples/dune @@ -21,6 +21,12 @@ (libraries tiny_httpd tiny_httpd_camlzip tiny_httpd_eio eio eio_posix)) +(executable + (name writer) + (flags :standard -warn-error -a+8) + (modules writer) + (libraries tiny_httpd)) + (rule (targets test_output.txt) (deps diff --git a/examples/writer.ml b/examples/writer.ml new file mode 100644 index 00000000..e00399f2 --- /dev/null +++ b/examples/writer.ml @@ -0,0 +1,62 @@ +module H = Tiny_httpd + +let serve_zeroes server : unit = + H.add_route_handler server H.(Route.(exact "zeroes" @/ int @/ return)) + @@ fun n _req -> + (* stream [n] zeroes *) + let write (oc : H.IO.Out_channel.t) : unit = + let buf = Bytes.make 1 '0' in + for _i = 1 to n do + H.IO.Out_channel.output oc buf 0 1 + done + in + let writer = H.IO.Writer.make ~write () in + H.Response.make_writer @@ Ok writer + +let serve_file server : unit = + H.add_route_handler server H.(Route.(exact "file" @/ string @/ return)) + @@ fun file _req -> + if Sys.file_exists file then ( + (* stream the content of the file *) + let write oc = + let buf = Bytes.create 4096 in + let ic = open_in file in + Fun.protect ~finally:(fun () -> close_in_noerr ic) @@ fun () -> + while + let n = input ic buf 0 (Bytes.length buf) in + if n > 0 then H.IO.Out_channel.output oc buf 0 n; + n > 0 + do + () + done + in + + let writer = H.IO.Writer.make ~write () in + H.Response.make_writer @@ Ok writer + ) else + H.Response.fail ~code:404 "file not found" + +let () = + let port = ref 8085 in + Arg.parse [ "-p", Arg.Set_int port, " port" ] ignore ""; + let server = H.create ~port:!port () in + Printf.printf "listen on http://localhost:%d/\n%!" !port; + serve_file server; + serve_zeroes server; + H.add_route_handler server H.Route.return (fun _req -> + let body = + H.Html.( + div [] + [ + p [] [ txt "routes" ]; + ul [] + [ + li [] + [ a [ A.href "/zeroes/1000" ] [ txt "get 1000 zeroes" ] ]; + li [] [ a [ A.href "/file/f_13M" ] [ txt "read file" ] ]; + ]; + ]) + |> H.Html.to_string_top + in + H.Response.make_string @@ Ok body); + H.run_exn server diff --git a/writer.sh b/writer.sh new file mode 100755 index 00000000..d8722cd8 --- /dev/null +++ b/writer.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec dune exec --display=quiet -- examples/writer.exe $@