Skip to content

Answer HEAD from the file's size when send_file would send it as it is - #815

Merged
sdogruyol merged 2 commits into
masterfrom
fix/head-skips-known-length-body
Sep 8, 2026
Merged

Answer HEAD from the file's size when send_file would send it as it is#815
sdogruyol merged 2 commits into
masterfrom
fix/head-skips-known-length-body

Conversation

@sdogruyol

Copy link
Copy Markdown
Member

The "separate" half of #803.

Description of the Change

HeadRequestHandler swaps the response output for a counting NullIO and runs the GET handler; Content-Length is whatever got written. For send_file that means a HEAD on a 20 GB file reads 20 GB off disk to count them. Measured with /proc/self/io on a 50 MiB file: HEAD read 50.0 MiB.

When the stored bytes go out as they are — coding.nil?, i.e. no on-the-fly compression, which also covers the pre-compressed .gz neighbour StaticFileHandler substitutes — the length is File.info.size, already in hand. send_file now sets Content-Length and returns before opening the body for copying. After: 0.0001 MiB read.

Two deliberate limits:

  • The file is still opened (File.open(file_path) { }, no read). A HEAD has to fail where the GET fails; otherwise it would report 200 and the size of a file the GET answers 404 for (Report an unreadable static file as 404, as the stdlib does #799). One open/close syscall, no bytes.
  • A body Kemal compresses is still produced. Its length isn't knowable without producing it, and head_request_handler_spec.cr:25 pins that a gzip HEAD reports the compressed length. Unchanged.

Verification

  • New spec: HEAD on a 4 MiB file → 200, empty body, Content-Length: 4194304 (portable).
  • New spec, Linux only (/proc/self/io): the HEAD reads under 1 MiB. Fails on master with 4194402 bytes read.
  • Extended the unreadable-file spec: HEAD on a mode-000 file is 404 like the GET.
  • Full suite green (498), -Dwithout_zlib green (467), ameba and format clean.

HeadRequestHandler learns Content-Length by producing the body into a
counting sink, so a HEAD on a 20 GB download read all 20 GB to discard
them. When the stored bytes go out unchanged - no on-the-fly compression,
or a pre-compressed neighbour already standing in - the length is the
file's own, so send_file sets it and returns. The file is still opened so
a HEAD fails exactly where the GET would; a body that gets compressed is
still produced, since its length is only known afterwards.

Refs #803
@sdogruyol
sdogruyol merged commit 233adea into master Sep 8, 2026
46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant