fix: extend proxy timeout for Office rendering
This commit is contained in:
+22
-1
@@ -535,6 +535,27 @@ func (a *API) runtimeOriginAllowed(r *http.Request) bool {
|
||||
|
||||
var runtimeTransport = &http.Transport{Proxy: nil, DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext, ResponseHeaderTimeout: 20 * time.Second, IdleConnTimeout: 90 * time.Second}
|
||||
|
||||
// Office rendering can take longer than the normal API header deadline (the
|
||||
// bundled converter defaults to 60 seconds). Reuse a separate connection pool
|
||||
// without extending authentication or unrelated runtime requests.
|
||||
var runtimeOfficeTransport = func() *http.Transport {
|
||||
t := runtimeTransport.Clone()
|
||||
t.ResponseHeaderTimeout = 120 * time.Second
|
||||
return t
|
||||
}()
|
||||
|
||||
type runtimeProxyTransport struct {
|
||||
normal, office http.RoundTripper
|
||||
}
|
||||
|
||||
func (t runtimeProxyTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
// ReverseProxy has already removed the instance prefix here.
|
||||
if r.URL.Path == "/api/officeToPdf/render" {
|
||||
return t.office.RoundTrip(r)
|
||||
}
|
||||
return t.normal.RoundTrip(r)
|
||||
}
|
||||
|
||||
type runtimeAuthKey struct {
|
||||
api *API
|
||||
id string
|
||||
@@ -1016,7 +1037,7 @@ func withoutRuntimeToken(rawQuery string) string {
|
||||
|
||||
func runtimeReverseProxy(target *url.URL, prefix, host, cookies string) *httputil.ReverseProxy {
|
||||
return &httputil.ReverseProxy{
|
||||
Transport: runtimeTransport, FlushInterval: -1, ErrorLog: log.New(io.Discard, "", 0),
|
||||
Transport: runtimeProxyTransport{normal: runtimeTransport, office: runtimeOfficeTransport}, FlushInterval: -1, ErrorLog: log.New(io.Discard, "", 0),
|
||||
Rewrite: func(p *httputil.ProxyRequest) {
|
||||
p.SetURL(target)
|
||||
p.Out.Host = host
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func officeTestProxy(t *testing.T, upstream string, officeTimeout time.Duration) *httputil.ReverseProxy {
|
||||
t.Helper()
|
||||
target, err := url.Parse(upstream)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
normal := runtimeTransport.Clone()
|
||||
office := runtimeOfficeTransport.Clone()
|
||||
normal.ResponseHeaderTimeout = 30 * time.Millisecond
|
||||
office.ResponseHeaderTimeout = officeTimeout
|
||||
t.Cleanup(normal.CloseIdleConnections)
|
||||
t.Cleanup(office.CloseIdleConnections)
|
||||
proxy := runtimeReverseProxy(target, "/dsh/office-test", "panel.test", "runtime=secret")
|
||||
proxy.Transport = runtimeProxyTransport{normal: normal, office: office}
|
||||
return proxy
|
||||
}
|
||||
|
||||
func TestRuntimeOfficeTransportDefaults(t *testing.T) {
|
||||
if got := runtimeTransport.ResponseHeaderTimeout; got != 20*time.Second {
|
||||
t.Errorf("normal timeout = %s, want 20s", got)
|
||||
}
|
||||
if got := runtimeOfficeTransport.ResponseHeaderTimeout; got != 120*time.Second {
|
||||
t.Errorf("office timeout = %s, want 120s", got)
|
||||
}
|
||||
if runtimeTransport == runtimeOfficeTransport {
|
||||
t.Fatal("office transport must be separate")
|
||||
}
|
||||
target, _ := url.Parse("http://panel.test")
|
||||
proxy := runtimeReverseProxy(target, "/dsh/office-test", "panel.test", "")
|
||||
transport, ok := proxy.Transport.(runtimeProxyTransport)
|
||||
if !ok || transport.normal != runtimeTransport || transport.office != runtimeOfficeTransport {
|
||||
t.Fatalf("unexpected default transport: %#v", proxy.Transport)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeOfficeProxyRouting(t *testing.T) {
|
||||
for _, path := range []string{
|
||||
"/api/officeToPdf/render",
|
||||
"/api/status",
|
||||
"/api/officeToPdf/render/",
|
||||
"/api/officeToPdf/render/child",
|
||||
"/api/officeToPdf/renderOther",
|
||||
"/api/officetopdf/render",
|
||||
"/api/officeToPdf/status",
|
||||
} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
const body = "document payload"
|
||||
const query = "file=report%20one.docx&mode=pdf&mode=preview"
|
||||
var calls atomic.Int32
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls.Add(1)
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil || string(data) != body || r.Method != http.MethodPost {
|
||||
t.Errorf("upstream method/body = %s %q, error %v", r.Method, data, err)
|
||||
}
|
||||
if r.URL.Path != path || r.URL.RawQuery != query {
|
||||
t.Errorf("upstream URL = %s", r.URL)
|
||||
}
|
||||
if r.Host != "panel.test" || r.Header.Get("X-Forwarded-Prefix") != "/dsh/office-test" || r.Header.Get("Cookie") != "runtime=secret" {
|
||||
t.Errorf("upstream host/headers = %s %v", r.Host, r.Header)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
w.Header().Set("Content-Type", "application/pdf")
|
||||
_, _ = io.WriteString(w, "rendered PDF")
|
||||
}))
|
||||
defer upstream.Close()
|
||||
proxy := officeTestProxy(t, upstream.URL, 500*time.Millisecond)
|
||||
req := httptest.NewRequest(http.MethodPost, "/dsh/office-test"+path+"?"+query+"&token=removed", strings.NewReader(body))
|
||||
req.Header.Set("Cookie", "panel=private")
|
||||
originalURL := req.URL.String()
|
||||
w := httptest.NewRecorder()
|
||||
proxy.ServeHTTP(w, req)
|
||||
want := http.StatusBadGateway
|
||||
if path == "/api/officeToPdf/render" {
|
||||
want = http.StatusOK
|
||||
if w.Body.String() != "rendered PDF" {
|
||||
t.Errorf("response body = %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
if w.Code != want {
|
||||
t.Errorf("status = %d, want %d: %s", w.Code, want, w.Body.String())
|
||||
}
|
||||
if req.URL.String() != originalURL || req.Header.Get("Cookie") != "panel=private" {
|
||||
t.Error("proxy mutated incoming URL or cookies")
|
||||
}
|
||||
upstream.Close()
|
||||
if got := calls.Load(); got != 1 {
|
||||
t.Errorf("mutating request sent %d times, want exactly once", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeOfficeProxyFiniteTimeoutAndCancellation(t *testing.T) {
|
||||
for _, name := range []string{"header timeout", "caller cancellation"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
started := make(chan struct{}, 2)
|
||||
canceled := make(chan struct{}, 2)
|
||||
var calls atomic.Int32
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls.Add(1)
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
started <- struct{}{}
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
canceled <- struct{}{}
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
}))
|
||||
defer upstream.Close()
|
||||
officeTimeout := 100 * time.Millisecond
|
||||
if name == "caller cancellation" {
|
||||
officeTimeout = 5 * time.Second
|
||||
}
|
||||
proxy := officeTestProxy(t, upstream.URL, officeTimeout)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
req := httptest.NewRequest(http.MethodPost, "/dsh/office-test/api/officeToPdf/render?mode=pdf", strings.NewReader("document")).WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
proxy.ServeHTTP(w, req)
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("request never reached upstream")
|
||||
}
|
||||
if name == "caller cancellation" {
|
||||
cancel()
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("proxy did not promptly finish")
|
||||
}
|
||||
if w.Code != http.StatusBadGateway {
|
||||
t.Errorf("status = %d, want 502", w.Code)
|
||||
}
|
||||
select {
|
||||
case <-canceled:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("upstream context was not canceled")
|
||||
}
|
||||
upstream.Close()
|
||||
if got := calls.Load(); got != 1 {
|
||||
t.Errorf("mutating request sent %d times, want exactly once", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user