Skip to content

Wrap the router in Rack::Head instead of each endpoint's stack - #3042

Open
ericproulx wants to merge 1 commit into
masterfrom
fix/head-around-router
Open

ericproulx wants to merge 1 commit into
masterfrom
fix/head-around-router

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

Rack::Head was the first middleware in each endpoint's stack, so it only stripped bodies from responses that went through one. Two kinds didn't:

  • a Rack app mounted with mount: the router calls it directly (config.app || build_stack), so a HEAD request got the app's body back, and raised Rack::Lint::LintError: Response body was given for HEAD request under lint!;
  • a middleware inserted with insert 0 (the README's CustomOverwriter example), which sat outside Rack::Head.

The router's own 404 had to strip its body itself (#2958) for the same reason.

This puts a single Rack::Head around the router in Grape::API::Instance, inside Rack::Lint so Lint checks the response that is served, and drops the router's 404 special case. A request that cascades through several routes goes through one Rack::Head instead of one per route tried.

This came up while looking at where Rack::TempfileReaper should go in #3041: the same config.app || build_stack gap means a per-endpoint middleware never runs for a mounted Rack app.

Contract change

An endpoint's stack now starts with Grape::Middleware::Error, so:

  • insert_before Rack::Head / insert_after Rack::Head raise No such middleware to insert before: Rack::Head when the API compiles;
  • a middleware inserted by index lands one place further in: insert 1 now goes inside Grape::Middleware::Error instead of between it and Rack::Head;
  • a middleware inserted with insert 0 sees the full body of a HEAD response, as one added with use always has.

Covered in UPGRADING.

Test plan

  • New specs: HEAD to a mounted Rack app, and HEAD answered by an insert 0 middleware, both come back without a body. Both fail against master's lib.
  • bundle exec rake: RuboCop clean, 3158 examples, 0 failures.
  • gemfiles/rack_2_2.gemfile: 3162 examples, 0 failures.
  • CI green.

🤖 Generated with Claude Code

Rack::Head was the first middleware in each endpoint's stack, so it only
saw responses that went through one. A Rack app mounted with `mount` is
called by the router directly and skips `build_stack`, and a middleware
inserted with `insert 0` sat outside Rack::Head: a HEAD request to
either got the full body back. The router's own 404 had to strip its
body itself (#2958) for the same reason.

A single Rack::Head around the router, inside Rack::Lint, now strips
the body of every HEAD response the API gives, and the router's 404
special case goes away. A request that cascades through several routes
goes through one Rack::Head instead of one per route tried.

Since an endpoint's stack no longer contains Rack::Head,
`insert_before`/`insert_after Rack::Head` raise when the API compiles,
and a middleware inserted by index lands one place further in. Both are
covered in UPGRADING.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the fix/head-around-router branch from 6fb8c7c to 096fc67 Compare October 24, 2026 08:06
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Danger Report

No issues found.

View run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants