TestDialErrorBodyRetry verifies that a POST request whose body has NOT been pre-buffered via request_buffers can still be retried after a dial error. Before the fix, a dial error caused Go's transport to close the shared body (via cloneRequest's shallow copy), so the retry attempt would read from a
(t *testing.T)
| 138 | // shared body. Since dial errors never read any bytes, the body remains at |
| 139 | // position 0 for the retry. |
| 140 | func TestDialErrorBodyRetry(t *testing.T) { |
| 141 | // Good upstream: echoes the request body with 200 OK. |
| 142 | goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 143 | body, err := io.ReadAll(r.Body) |
| 144 | if err != nil { |
| 145 | http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError) |
| 146 | return |
| 147 | } |
| 148 | w.WriteHeader(http.StatusOK) |
| 149 | _, _ = w.Write(body) |
| 150 | })) |
| 151 | t.Cleanup(goodServer.Close) |
| 152 | |
| 153 | const requestBody = "hello, retry" |
| 154 | |
| 155 | tests := []struct { |
| 156 | name string |
| 157 | method string |
| 158 | body string |
| 159 | retries int |
| 160 | wantStatus int |
| 161 | wantBody string |
| 162 | }{ |
| 163 | { |
| 164 | // Core regression case: POST with a body, no request_buffers, |
| 165 | // dial error on first upstream → retry to second upstream succeeds. |
| 166 | name: "POST body retried after dial error", |
| 167 | method: http.MethodPost, |
| 168 | body: requestBody, |
| 169 | retries: 1, |
| 170 | wantStatus: http.StatusOK, |
| 171 | wantBody: requestBody, |
| 172 | }, |
| 173 | { |
| 174 | // Dial errors are always retried regardless of method, but there |
| 175 | // is no body to re-read, so GET has always worked. Keep it as a |
| 176 | // sanity check that we did not break the no-body path. |
| 177 | name: "GET without body retried after dial error", |
| 178 | method: http.MethodGet, |
| 179 | body: "", |
| 180 | retries: 1, |
| 181 | wantStatus: http.StatusOK, |
| 182 | wantBody: "", |
| 183 | }, |
| 184 | { |
| 185 | // Without any retry configuration the handler must give up on the |
| 186 | // first dial error and return a 502. Confirms no wrapping occurs |
| 187 | // in the no-retry path. |
| 188 | name: "no retries configured returns 502 on dial error", |
| 189 | method: http.MethodPost, |
| 190 | body: requestBody, |
| 191 | retries: 0, |
| 192 | wantStatus: http.StatusBadGateway, |
| 193 | wantBody: "", |
| 194 | }, |
| 195 | } |
| 196 | |
| 197 | for _, tc := range tests { |
nothing calls this directly
no test coverage detected