How to serve a file

View Source

Problem

You want to send a file from disk as the response body without reading it all into memory first.

Solution

livery_resp:file(200, <<"/var/www/index.html">>).

Livery streams the file in 64 KiB chunks straight to the wire on H1, H2, and H3. Content-Length is set from the file size unless your handler already set it.

Set the content type yourself; Livery does not guess it:

fun(_Req) ->
    R = livery_resp:file(200, <<"/var/www/app.css">>),
    livery_resp:with_header(<<"content-type">>, <<"text/css">>, R)
end.

Serve a byte range

Pass {Offset, Length} to send a slice. Length may be eof to read to the end of the file. Livery adds a Content-Range header and Content-Length for the slice:

%% bytes 1024-2047 of the file
livery_resp:file(206, Path, {1024, 1024}).

%% from byte 1024 to the end
livery_resp:file(206, Path, {1024, eof}).

Set the status to 206 yourself when you serve a partial range.

Security: never pass unsanitised paths

livery_resp:file/2,3 serves exactly the path you give it; Livery does not confine it to a directory. If you build the path from request data (a path parameter, query string, header), an attacker can use .. to escape your intended root and read arbitrary files.

Confine it yourself before serving:

serve_asset(Req) ->
    Name = livery_req:binding(<<"name">>, Req),
    Root = <<"/var/www/assets">>,
    Path = filename:join(Root, Name),
    %% Reject anything that resolves outside Root.
    Safe = filename:absname(Path),
    case binary:match(filename:absname(Path), filename:absname(Root)) of
        {0, _} -> livery_resp:file(200, Safe);
        _      -> livery_resp:text(403, <<"forbidden">>)
    end.

Prefer an allowlist of known filenames where you can.

Error handling

SituationResponse
File does not exist404
Range starts past the end of the file416
Path is a directory or unreadablestream reset

See also