Fix git smart-HTTP streaming: apply CGI headers before chunking
5e67fc0 · gmorell · 2026-06-25 15:14
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud_web/plugs/git_smart_http.ex
+15
−6
@@ -78,10 +78,12 @@ defmodule GitGudWeb.Plugs.GitSmartHttp do
| 78 | 78 | when service in [@upload_pack, @receive_pack] do |
| 79 | 79 | with {:ok, repo} <- find_repo(owner, name), |
| 80 | 80 | {:ok, conn} <- authorize(conn, repo, service) do |
| 81 | + # Don't send_chunked here — the CGI emits its own status + headers | |
| 82 | + # (Content-Type, etc.) which stream_cgi/relay_response apply before | |
| 83 | + # chunking. Chunking first makes apply_cgi_headers raise | |
| 84 | + # Plug.Conn.AlreadySentError and resets the HTTP/2 stream. | |
| 81 | 85 | conn |
| 82 | − |> put_resp_content_type("application/x-#{service}-advertisement", nil) | |
| 83 | 86 | |> put_resp_header("cache-control", "no-cache") |
| 84 | − |> send_chunked(200) | |
| 85 | 87 | |> stream_cgi(repo, ["http-backend"], cgi_env(conn, repo, service, "GET", true)) |
| 86 | 88 | else |
| 87 | 89 | {:error, :not_found} -> send_resp(conn, 404, "") |
@@ -95,10 +97,9 @@ defmodule GitGudWeb.Plugs.GitSmartHttp do
| 95 | 97 | defp handle_rpc(conn, owner, name, service) do |
| 96 | 98 | with {:ok, repo} <- find_repo(owner, name), |
| 97 | 99 | {:ok, conn} <- authorize(conn, repo, service) do |
| 100 | + # See handle_info_refs: let the CGI's headers land before chunking. | |
| 98 | 101 | conn |
| 99 | − |> put_resp_content_type("application/x-#{service}-result", nil) | |
| 100 | 102 | |> put_resp_header("cache-control", "no-cache") |
| 101 | − |> send_chunked(200) | |
| 102 | 103 | |> stream_cgi(repo, ["http-backend"], cgi_env(conn, repo, service, "POST", false)) |
| 103 | 104 | else |
| 104 | 105 | {:error, :not_found} -> send_resp(conn, 404, "") |
@@ -318,9 +319,12 @@ defmodule GitGudWeb.Plugs.GitSmartHttp do
| 318 | 319 | end |
| 319 | 320 | |
| 320 | 321 | {^port, {:exit_status, _}} -> |
| 321 | − conn | |
| 322 | + # CGI exited before emitting a header block — send a real error | |
| 323 | + # instead of returning an unsent conn (Bandit would reject it). | |
| 324 | + send_resp(conn, 500, "git http-backend produced no output") | |
| 322 | 325 | after |
| 323 | − 60_000 -> conn | |
| 326 | + 60_000 -> | |
| 327 | + send_resp(conn, 504, "git http-backend timed out") | |
| 324 | 328 | end |
| 325 | 329 | end |
| 326 | 330 |
@@ -338,6 +342,11 @@ defmodule GitGudWeb.Plugs.GitSmartHttp do
| 338 | 342 | end |
| 339 | 343 | |
| 340 | 344 | defp relay_response_send(conn, port, initial) do |
| 345 | + # Now that the CGI's status + headers are on the conn, start the | |
| 346 | + # chunked response (using the CGI status if it set one, else 200), | |
| 347 | + # then stream the body. | |
| 348 | + conn = send_chunked(conn, conn.status || 200) | |
| 349 | + | |
| 341 | 350 | conn = |
| 342 | 351 | if byte_size(initial) > 0 do |
| 343 | 352 | {:ok, conn} = Plug.Conn.chunk(conn, initial) |
Parents: 636a67b