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
+}