rc: respond with 202 if prefer-async header is passed

Make rc respond with a 202 status code (instead of 200) if `Prefer: respond-
async` was passed. Keeps backwards compatibility for current clients while also
allowing the OpenAPI schema & generators to differentiate the responses
properly.
This commit is contained in:
FTCHD
2026-05-25 19:50:41 +01:00
committed by GitHub
parent 675806067a
commit 605eb30674
3 changed files with 65 additions and 12 deletions
+7
View File
@@ -267,6 +267,13 @@ It is recommended that potentially long running jobs, e.g. `sync/sync`,
flag to avoid any potential problems with the HTTP request and
response timing out.
As an alternative to `_async`, you can send the HTTP header
`Prefer: respond-async` (RFC 7240). This has the same effect as
`_async = true` but additionally returns HTTP status 202 (Accepted)
instead of 200, and includes a `Preference-Applied: respond-async`
response header. The 202 status code makes it easy for clients to distinguish
an async response from a completed one without inspecting the body.
Starting a job with the `_async` flag:
```console
+15
View File
@@ -257,6 +257,17 @@ func (s *Server) handlePost(w http.ResponseWriter, r *http.Request, path string)
return
}
}
// Check for Prefer: respond-async header (RFC 7240)
preferAsync := false
for _, pref := range strings.Split(r.Header.Get("Prefer"), ",") {
if strings.EqualFold(strings.TrimSpace(pref), "respond-async") {
preferAsync = true
in["_async"] = true
break
}
}
// Find the call
call := rc.Calls.Get(path)
if call == nil {
@@ -296,6 +307,10 @@ func (s *Server) handlePost(w http.ResponseWriter, r *http.Request, path string)
fs.Debugf(nil, "rc: %q: reply %+v: %v", path, out, err)
w.Header().Set("Content-Type", "application/json")
if preferAsync {
w.Header().Set("Preference-Applied", "respond-async")
w.WriteHeader(http.StatusAccepted)
}
err = rc.WriteJSON(w, out)
if err != nil {
// can't return the error at this point - but have a go anyway
+43 -12
View File
@@ -89,18 +89,19 @@ func TestRcServer(t *testing.T) {
}
type testRun struct {
Name string
URL string
User string
Pass string
Status int
Method string
Range string
Body string
ContentType string
Expected string
Contains *regexp.Regexp
Headers map[string]string
Name string
URL string
User string
Pass string
Status int
Method string
Range string
Body string
ContentType string
Expected string
Contains *regexp.Regexp
Headers map[string]string
RequestHeaders map[string]string
}
// Run a suite of tests
@@ -144,6 +145,9 @@ func emulateCalls(t *testing.T, tests []testRun, mux chi.Router, testURL string)
if test.User != "" && test.Pass != "" {
req.SetBasicAuth(test.User, test.Pass)
}
for k, v := range test.RequestHeaders {
req.Header.Set(k, v)
}
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
@@ -794,6 +798,33 @@ func TestRCAsync(t *testing.T) {
Body: `{ "_async":true }`,
Status: http.StatusOK,
Contains: regexp.MustCompile(`(?s)\{.*\"jobid\":.*\}`),
}, {
Name: "prefer-respond-async",
URL: "rc/noop",
Method: "POST",
ContentType: "application/json",
Body: `{ "_async":true }`,
RequestHeaders: map[string]string{"Prefer": "respond-async"},
Status: http.StatusAccepted,
Contains: regexp.MustCompile(`(?s)\{.*\"jobid\":.*\}`),
}, {
Name: "prefer-without-async",
URL: "rc/noop",
Method: "POST",
ContentType: "application/json",
Body: `{}`,
RequestHeaders: map[string]string{"Prefer": "respond-async"},
Status: http.StatusAccepted,
Contains: regexp.MustCompile(`(?s)\{.*\"jobid\":.*\}`),
}, {
Name: "prefer-respond-async-mixed",
URL: "rc/noop",
Method: "POST",
ContentType: "application/json",
Body: `{}`,
RequestHeaders: map[string]string{"Prefer": "wait=10, respond-async"},
Status: http.StatusAccepted,
Contains: regexp.MustCompile(`(?s)\{.*\"jobid\":.*\}`),
}, {
Name: "bad",
URL: "rc/noop",