From 605eb306741d398fb720ce381f25e125b86c9281 Mon Sep 17 00:00:00 2001 From: FTCHD <144691102+FTCHD@users.noreply.github.com> Date: Mon, 25 May 2026 21:50:41 +0300 Subject: [PATCH] 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. --- docs/content/rc.md | 7 +++++ fs/rc/rcserver/rcserver.go | 15 +++++++++ fs/rc/rcserver/rcserver_test.go | 55 ++++++++++++++++++++++++++------- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/docs/content/rc.md b/docs/content/rc.md index 4ae9c46f8..b4a99ffe3 100644 --- a/docs/content/rc.md +++ b/docs/content/rc.md @@ -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 diff --git a/fs/rc/rcserver/rcserver.go b/fs/rc/rcserver/rcserver.go index 6be026e18..0278e5089 100644 --- a/fs/rc/rcserver/rcserver.go +++ b/fs/rc/rcserver/rcserver.go @@ -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 diff --git a/fs/rc/rcserver/rcserver_test.go b/fs/rc/rcserver/rcserver_test.go index 11b21d5f5..6854f7e97 100644 --- a/fs/rc/rcserver/rcserver_test.go +++ b/fs/rc/rcserver/rcserver_test.go @@ -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",