Malachi.Console.Static (malachi v0.22.7)

View Source

Serves the console bundle from its manifest (Malachi.Console.Assets), and nothing else.

A request path is percent-decoded segment by segment and looked up in the manifest as an exact key. It is never joined onto a directory, so there is no path to traverse: a segment that decodes to ., .. or empty, or to anything holding /, \ or a NUL byte, is refused with 400 before the lookup, because no manifest key can contain one and a client sending one is not asking for a file.

What a hit gets:

  • the representation chosen from Accept-Encoding by q-value: br or gzip when the file shipped that precompressed variant and the client gives it a q above zero (explicitly, or through *), the highest q winning and brotli breaking a tie; otherwise the uncompressed bytes. Compression never happens at request time;
  • ETag (the SHA-256 of that representation), Cache-Control (immutable for a name under assets/ whose last segment looks like a build hash, no-cache otherwise) and Vary: Accept-Encoding when the file has a variant;
  • 304 with those same headers and no body when If-None-Match matches, compared weakly as RFC 9110 section 13.1.2 requires: a list, *, and W/ validators all count.

A miss outside assets/ whose last segment names no known file type (no extension, or one the MIME table does not know, as in a topic named orders.v1) and in which no segment starts with a dot is a route of the single page application, answered with index.html under the same rules. Any other miss is 404: a missing script must not come back as HTML with a 200, which a browser would try to execute and report as an opaque syntax error.

Before anything is sent, the file is checked against what was recorded at startup (Malachi.Console.Assets.unchanged?/1): a file or a directory above it replaced by a symbolic link, a file rewritten with another size or a newer modification time, and a file removed all answer 404 rather than bytes the ETag does not describe or a file outside the static root. It is a check before the send, so a rewrite that keeps the size and the modification time, or a swap made between the check and the send, is not caught.

Range requests are not supported and no Accept-Ranges is sent, which RFC 9110 permits.

Summary

Functions

Picks the representation to send for the Accept-Encoding header values given: the precompressed variant with the highest q above zero, brotli winning a tie, else identity. Returns the coding name and its variant.

Whether If-None-Match header values match etag, by the weak comparison RFC 9110 prescribes for this header: * matches anything, and W/ is ignored on both sides.

Sends one of the endpoint's constant plain text answers, which no cache may keep, and halts. Every answer that is not a file goes through here, so they share one content type and one cache rule, and each body is a literal in its own clause: nothing from the request can reach a response body.

Functions

choose(variants, accept_encoding)

Picks the representation to send for the Accept-Encoding header values given: the precompressed variant with the highest q above zero, brotli winning a tie, else identity. Returns the coding name and its variant.

fresh?(if_none_match, etag)

@spec fresh?([String.t()], String.t()) :: boolean()

Whether If-None-Match header values match etag, by the weak comparison RFC 9110 prescribes for this header: * matches anything, and W/ is ignored on both sides.

plain(conn, atom)

@spec plain(
  Plug.Conn.t(),
  :bad_request | :not_found | :method_not_allowed | :not_built
) ::
  Plug.Conn.t()

Sends one of the endpoint's constant plain text answers, which no cache may keep, and halts. Every answer that is not a file goes through here, so they share one content type and one cache rule, and each body is a literal in its own clause: nothing from the request can reach a response body.