Skip to content

fix(endpoint): set connection close header on reraised errors - #6866

Open
javiergarea wants to merge 1 commit into
phoenixframework:mainfrom
javiergarea:fix/connection-close-header-on-reraised-errors
Open

javiergarea wants to merge 1 commit into
phoenixframework:mainfrom
javiergarea:fix/connection-close-header-on-reraised-errors

Conversation

@javiergarea

Copy link
Copy Markdown

Closes #6859

Description

RenderErrors renders the error response and then reraises, after which both bandit and cowboy close the HTTP/1 connection.
The response never advertised the closure, so clients reusing the connection hit resets.
Per RFC 9112 §9.6, this PR sets connection: close on error responses that will be reraised.

  • NoRouteError is not reraised, so no header is added there
  • HTTP/2 is untouched (connection headers forbidden, stream reset only)
  • No connection behaviour changes, the header only advertises the close that already happens on both adapters

Manual validation

Server, from a phoenix checkout (MIX_ENV=test iex -S mix):

defmodule ErrorHTML do
  def render("500.html", _), do: "oops"
end

Application.put_env(:phoenix, Srv,
  http: [port: 4000],
  server: true,
  # Adapters
  adapter: Bandit.PhoenixAdapter,
  # adapter: Phoenix.Endpoint.Cowboy2Adapter,
  render_errors: [formats: [html: ErrorHTML], layout: false],
  secret_key_base: String.duplicate("a", 64)
)

defmodule Srv do
  use Phoenix.Endpoint, otp_app: :phoenix
  plug :boom
  def boom(_conn, _opts), do: raise("boom")
end

Srv.start_link()

Client, in a separate plain iex:

{:ok, s} = :gen_tcp.connect(~c"localhost", 4000, [:binary, active: false])
:gen_tcp.send(s, "GET / HTTP/1.1\r\nhost: localhost\r\n\r\n")
:gen_tcp.recv(s, 0, 5_000)
:gen_tcp.recv(s, 0, 10_000)

On this branch, both adapters:

{:ok, "HTTP/1.1 500 Internal Server Error\r\n...connection: close\r\n..."}
{:error, :closed}

On main, both adapters: the same minus the connection header.

@SteffenDE

Copy link
Copy Markdown
Member

Since Bandit and Cowboy are the ones that close the connection, can't they ensure the header is set?

@javiergarea

Copy link
Copy Markdown
Author

Since Bandit and Cowboy are the ones that close the connection, can't they ensure the header is set?

That was my first thought too, but the timing seems to get in the way:

  1. Phoenix renders the error response
  2. the server sends the headers and picks keep-alive, since the response still looks regular at this point
  3. Phoenix re-raises
  4. the server catches the exception and closes the connection

The error reaches the server at step 4, but the headers are gone since step 2.
The window where the close is known and the headers are still writable seems to be step 1, after catching and before re-raising.

This branch has not been deployed

No deployments
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.

Connection: close header not sent before closing HTTP 1.1 connection when a controller raises an exception

2 participants