feat(plugins): add NoFollowRedirects option to HTTPRequest
Allow plugins to opt out of automatic redirect following on a per-request basis. When set to true, the response returns the redirect status code and Location header directly instead of following to the final destination.
This commit is contained in:
@@ -7,6 +7,7 @@ type HTTPRequest struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Headers map[string]string `json:"headers,omitempty"`
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
|
NoFollowRedirects bool `json:"noFollowRedirects,omitempty"`
|
||||||
Body []byte `json:"body,omitempty"`
|
Body []byte `json:"body,omitempty"`
|
||||||
TimeoutMs int32 `json:"timeoutMs,omitempty"`
|
TimeoutMs int32 `json:"timeoutMs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const (
|
|||||||
httpClientMaxResponseBodyLen = 10 * 1024 * 1024 // 10 MB
|
httpClientMaxResponseBodyLen = 10 * 1024 * 1024 // 10 MB
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// contextKey is used for per-request redirect control via context.
|
||||||
|
type contextKey struct{}
|
||||||
|
|
||||||
|
// noFollowRedirectsKey signals the CheckRedirect callback to stop following redirects.
|
||||||
|
var noFollowRedirectsKey = contextKey{}
|
||||||
|
|
||||||
// httpServiceImpl implements host.HTTPService.
|
// httpServiceImpl implements host.HTTPService.
|
||||||
type httpServiceImpl struct {
|
type httpServiceImpl struct {
|
||||||
pluginName string
|
pluginName string
|
||||||
@@ -44,6 +50,9 @@ func newHTTPService(pluginName string, permission *HTTPPermission) *httpServiceI
|
|||||||
// Timeout is set per-request via context deadline, not here.
|
// Timeout is set per-request via context deadline, not here.
|
||||||
// CheckRedirect validates hosts and enforces redirect limits.
|
// CheckRedirect validates hosts and enforces redirect limits.
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if req.Context().Value(noFollowRedirectsKey) != nil {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
if len(via) >= httpClientMaxRedirects {
|
if len(via) >= httpClientMaxRedirects {
|
||||||
log.Warn(req.Context(), "HTTP redirect limit exceeded", "plugin", svc.pluginName, "url", req.URL.String(), "redirectCount", len(via))
|
log.Warn(req.Context(), "HTTP redirect limit exceeded", "plugin", svc.pluginName, "url", req.URL.String(), "redirectCount", len(via))
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
@@ -80,6 +89,11 @@ func (s *httpServiceImpl) Send(ctx context.Context, request host.HTTPRequest) (*
|
|||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
// Signal CheckRedirect to not follow redirects for this request
|
||||||
|
if request.NoFollowRedirects {
|
||||||
|
ctx = context.WithValue(ctx, noFollowRedirectsKey, true)
|
||||||
|
}
|
||||||
|
|
||||||
// Build request body
|
// Build request body
|
||||||
method := strings.ToUpper(request.Method)
|
method := strings.ToUpper(request.Method)
|
||||||
var body io.Reader
|
var body io.Reader
|
||||||
|
|||||||
@@ -311,6 +311,26 @@ var _ = Describe("httpServiceImpl", func() {
|
|||||||
Expect(err.Error()).To(ContainSubstring("context canceled"))
|
Expect(err.Error()).To(ContainSubstring("context canceled"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("should not follow redirects when NoFollowRedirects is true", func() {
|
||||||
|
dest := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, _ = w.Write([]byte("final"))
|
||||||
|
}))
|
||||||
|
defer dest.Close()
|
||||||
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, dest.URL, http.StatusFound)
|
||||||
|
}))
|
||||||
|
resp, err := svc.Send(context.Background(), host.HTTPRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: ts.URL,
|
||||||
|
TimeoutMs: 1000,
|
||||||
|
NoFollowRedirects: true,
|
||||||
|
})
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(resp.StatusCode).To(Equal(int32(302)))
|
||||||
|
Expect(resp.Headers["Location"]).To(Equal(dest.URL))
|
||||||
|
Expect(string(resp.Body)).ToNot(Equal("final"))
|
||||||
|
})
|
||||||
|
|
||||||
It("should send request headers", func() {
|
It("should send request headers", func() {
|
||||||
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte(r.Header.Get("X-Custom")))
|
_, _ = w.Write([]byte(r.Header.Get("X-Custom")))
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type HTTPRequest struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Headers map[string]string `json:"headers"`
|
Headers map[string]string `json:"headers"`
|
||||||
|
NoFollowRedirects bool `json:"noFollowRedirects"`
|
||||||
Body []byte `json:"body"`
|
Body []byte `json:"body"`
|
||||||
TimeoutMs int32 `json:"timeoutMs"`
|
TimeoutMs int32 `json:"timeoutMs"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type HTTPRequest struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Headers map[string]string `json:"headers"`
|
Headers map[string]string `json:"headers"`
|
||||||
|
NoFollowRedirects bool `json:"noFollowRedirects"`
|
||||||
Body []byte `json:"body"`
|
Body []byte `json:"body"`
|
||||||
TimeoutMs int32 `json:"timeoutMs"`
|
TimeoutMs int32 `json:"timeoutMs"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ pub struct HTTPRequest {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub headers: std::collections::HashMap<String, String>,
|
pub headers: std::collections::HashMap<String, String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub no_follow_redirects: bool,
|
||||||
|
#[serde(default)]
|
||||||
#[serde(with = "base64_bytes")]
|
#[serde(with = "base64_bytes")]
|
||||||
pub body: Vec<u8>,
|
pub body: Vec<u8>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
Reference in New Issue
Block a user