SagaSaga
Edda

Forms, Cookies, and Static Files

This page covers the practical helpers that sit beside routing: decoded request bodies, cookies, signed cookies, and file serving.

URL-Encoded Forms

Use form_values for application/x-www-form-urlencoded request bodies and query_values for decoded query strings. Both return FormValues, which preserves insertion order and duplicate field names.

case form_values req {
  Ok values -> case form_get "name" values {
    Just name -> text 200 $"hello {name}"
    Nothing -> text 400 "missing name"
  }
  Err _ -> text 400 "bad form"
}

Use form_get_all for repeated names like checkbox groups or tags. Parser limits come from RequestParserConfig; create_app installs defaults, and use_options changes them app-wide.

Multipart Forms

Use multipart_values for buffered multipart/form-data. It returns MultipartFormValues, whose entries are either MultipartText String or MultipartFileValue MultipartFile.

case multipart_values req {
  Ok values -> case multipart_file "upload" values {
    Just file -> text 200 $"received {file.filename}"
    Nothing -> text 400 "missing file"
  }
  Err _ -> text 400 "bad multipart body"
}

Buffered multipart parsing keeps each text value and file body in memory. Size limits are enabled by default; use use_options or multipart_values_with to tune them. For saving client filenames, call sanitize_filename before treating a filename as a local basename.

For large uploads, streaming handlers can parse multipart bodies incrementally:

stream_multipart req [] on_text on_file

Use stream_multipart_to_temp_files when the app wants Edda to write uploaded files to caller-owned temp paths and return metadata. Call delete_multipart_temp_files after the app is done with those files.

Cookies

cookies req parses all Cookie headers into ordered (name, value) pairs. cookie "session" req returns the first matching value.

Response helpers append Set-Cookie headers:

text 200 "ok"
|> set_cookie "theme" "dark"
|> set_cookie_with "session" "abc" default_session_cookie_options

Use delete_cookie to expire a cookie. The checked variants validate cookie names and return Result Response CookieError.

Signed Cookies

Edda.Crypto signs cookie values with HMAC. Signed cookies are tamper-evident, not encrypted.

let secret = cookie_secret "dev-only-secret"

case signed_cookie secret "session" req {
  Ok user -> text 200 $"welcome {user}"
  Err _ -> redirect 303 "/login"
}

For rotation, use cookie_secrets_with_previous; new cookies are signed with the current secret, while verification accepts previous secrets.

Static Files

Serve a directory under a URL prefix:

choose [
  static_dir "/static" "src/static",
  route GET "/" home,
] req

static_dir supports GET and HEAD, infers common content types, rejects unsafe path segments, emits ETag and Last-Modified, handles conditional requests, and supports single byte ranges. Unsupported multi-range requests are ignored and receive the full response body.

Use static_dir_with to set Cache-Control or disable index-file lookup. When index lookup is disabled, requests for the directory root return 404:

let options =
  default_static_options
  |> static_immutable_cache 31536000

static_dir_with options "/assets" "priv/assets" req

For exact cache policies, use with_static_cache_control. Common helpers include static_no_cache, static_no_store, static_public_cache, and static_immutable_cache.

Use file_response or file_response_as when a route needs to serve one known filesystem path.

For large files, use the streaming variants:

static_dir_streaming "/static" "src/static"
file_stream_response "src/static/cats/cat1.jpg"

Streaming file responses avoid buffering the whole successful file response in memory while preserving validators, HEAD, and single byte ranges.