@marcel91200 / caddy Mirror · read-only / commits / 1962b204
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 02:15 UTC

fastcgi: Use 302 for a Location response without Status (#8209)

A CGI script that redirects by printing only a Location header got a 200 response with that header, which clients do not follow. RFC 3875 section 6.2.3 says the server sends 302 Found for this response, and net/http/cgi, nginx and Apache do so.

Jay Hemnani committed Oct 13, 2026 at 19:57 UTC 1962b2040127d4a6649e9c83ef240c3eaef236f4
2 files changed +98
modules/caddyhttp/reverseproxy/fastcgi/client.go
+2
@@ -253,6 +253,8 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
253 if statusIsCut {
254 resp.Status = statusInfo
255 }
256 + } else if resp.Header.Get("Location") != "" {
257 + resp.StatusCode = http.StatusFound
258 } else {
259 resp.StatusCode = http.StatusOK
260 }
modules/caddyhttp/reverseproxy/fastcgi/redirect_test.go new
+96
@@ -0,0 +1,96 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package fastcgi
16 +
17 +import (
18 + "bytes"
19 + "io"
20 + "net"
21 + "net/http"
22 + "net/http/httptest"
23 + "testing"
24 +)
25 +
26 +// cgiBackend answers one FastCGI request with stdout as the raw CGI response,
27 +// the way fcgiwrap passes on what a CGI script prints.
28 +func cgiBackend(t *testing.T, stdout string) string {
29 + t.Helper()
30 +
31 + ln, err := net.Listen("tcp", "127.0.0.1:0")
32 + if err != nil {
33 + t.Fatalf("listen: %v", err)
34 + }
35 + t.Cleanup(func() { ln.Close() })
36 +
37 + go func() {
38 + conn, err := ln.Accept()
39 + if err != nil {
40 + return
41 + }
42 + defer conn.Close()
43 +
44 + w := &streamWriter{c: &client{rwc: conn, reqID: 1}, buf: new(bytes.Buffer), recType: Stdout}
45 + if _, err := w.Write([]byte(stdout)); err != nil {
46 + return
47 + }
48 + if err := w.FlushStream(); err != nil {
49 + return
50 + }
51 + if err := w.writeRecord(EndRequest, make([]byte, 8)); err != nil {
52 + return
53 + }
54 + _, _ = io.Copy(io.Discard, conn)
55 + }()
56 +
57 + return ln.Addr().String()
58 +}
59 +
60 +func TestResponseStatus(t *testing.T) {
61 + tests := []struct {
62 + name string
63 + stdout string
64 + want int
65 + }{
66 + {
67 + name: "no status",
68 + stdout: "Content-Type: text/plain\r\n\r\nhello",
69 + want: http.StatusOK,
70 + },
71 + {
72 + name: "location without status",
73 + stdout: "Location: https://example.com/new\r\n\r\n",
74 + want: http.StatusFound,
75 + },
76 + {
77 + name: "location with status",
78 + stdout: "Status: 301 Moved Permanently\r\nLocation: https://example.com/new\r\n\r\n",
79 + want: http.StatusMovedPermanently,
80 + },
81 + }
82 + for _, tt := range tests {
83 + t.Run(tt.name, func(t *testing.T) {
84 + addr := cgiBackend(t, tt.stdout)
85 + req := httptest.NewRequest(http.MethodGet, "http://example.com/index.php", nil)
86 +
87 + res, err := fcgiRoundTrip(t, addr, req)
88 + if err != nil {
89 + t.Fatalf("round trip: %v", err)
90 + }
91 + if res.StatusCode != tt.want {
92 + t.Errorf("status = %d, want %d", res.StatusCode, tt.want)
93 + }
94 + })
95 + }
96 +}