package surf import ( "context" "fmt" "io" "net" "time" "github.com/enetx/g" "github.com/enetx/http" "github.com/enetx/surf/header" "github.com/enetx/surf/internal/drainbody" "github.com/enetx/surf/profiles/chrome" "github.com/enetx/surf/profiles/firefox" ) // Request represents an HTTP request with additional surf-specific functionality. // It wraps the standard http.Request and provides enhanced features like middleware support, // retry capabilities, remote address tracking, and structured error handling. type Request struct { request *http.Request // The underlying standard HTTP request cli *Client // The associated surf client for this request werr *error // Pointer to error encountered during request writing/preparation err error // General error associated with the request (validation, setup, etc.) remoteAddr net.Addr // Remote server address captured during connection body io.ReadCloser // Request body reader (for retry support and body preservation) } // GetRequest returns the underlying standard http.Request. // Provides access to the wrapped HTTP request for advanced use cases. func (req *Request) GetRequest() *http.Request { return req.request } // Do executes the HTTP request and returns a Response wrapped in a Result type. // This is the main method that performs the actual HTTP request with full surf functionality: // - Applies request middleware (authentication, headers, tracing, etc.) // - Preserves request body for potential retries // - Implements retry logic with configurable status codes and delays // - Measures request timing for performance analysis // - Handles request preparation errors and write errors func (req *Request) Do() g.Result[*Response] { // Return early if request has preparation errors if req.err != nil { return g.Err[*Response](req.err) } // Apply all configured request middleware if err := req.cli.applyReqMW(req); err != nil { return g.Err[*Response](err) } // Preserve request body for retries (except HEAD requests which have no body) if req.request.Method != http.MethodHead { req.body, req.request.Body, req.err = drainbody.DrainBody(req.request.Body) if req.err != nil { return g.Err[*Response](req.err) } } var ( resp *http.Response attempts int err error ) start := time.Now() cli := req.cli.cli builder := req.cli.builder // Execute request with retry logic retry: resp, err = cli.Do(req.request) if err != nil { return g.Err[*Response](err) } // Check if retry is needed based on status code and retry configuration if builder != nil && builder.retryMax != 0 && attempts < builder.retryMax && builder.retryCodes.NotEmpty() && builder.retryCodes.Contains(resp.StatusCode) { attempts++ time.Sleep(builder.retryWait) goto retry } // Check for write errors that occurred during request preparation if req.werr != nil && *req.werr != nil { return g.Err[*Response](*req.werr) } response := &Response{ Attempts: attempts, Time: time.Since(start), Client: req.cli, ContentLength: resp.ContentLength, Cookies: resp.Cookies(), Headers: Headers(resp.Header), Proto: g.String(resp.Proto), StatusCode: StatusCode(resp.StatusCode), URL: resp.Request.URL, UserAgent: g.String(req.request.UserAgent()), remoteAddr: req.remoteAddr, request: req, response: resp, } if req.request.Method != http.MethodHead { response.Body = new(Body) response.Body.Reader = resp.Body response.Body.cache = builder != nil && builder.cacheBody response.Body.contentType = resp.Header.Get(header.CONTENT_TYPE) response.Body.limit = -1 } if err := req.cli.applyRespMW(response); err != nil { return g.Err[*Response](err) } return g.Ok(response) } // WithContext associates a context with the request for cancellation and deadlines. // The context can be used to cancel the request, set timeouts, or pass request-scoped values. // Returns the request for method chaining. If ctx is nil, the request is unchanged. func (req *Request) WithContext(ctx context.Context) *Request { if ctx != nil { req.request = req.request.WithContext(ctx) } return req } // AddCookies adds one or more HTTP cookies to the request. // Cookies are added to the request headers and will be sent with the HTTP request. // Returns the request for method chaining. func (req *Request) AddCookies(cookies ...*http.Cookie) *Request { for _, cookie := range cookies { req.request.AddCookie(cookie) } return req } // SetHeaders sets HTTP headers for the request, replacing any existing headers with the same name. // Supports multiple input formats: // - Two arguments: key, value (string or g.String) // - Single argument: http.Header, Headers, map types, or g.Map types // Maintains header order for fingerprinting purposes when using g.MapOrd. // Returns the request for method chaining. func (req *Request) SetHeaders(headers ...any) *Request { if req.request == nil || headers == nil { return req } req.applyHeaders(headers, func(h http.Header, k, v string) { h.Set(k, v) }) return req } // AddHeaders adds HTTP headers to the request, appending to any existing headers with the same name. // Unlike SetHeaders, this method preserves existing headers and adds new values. // Supports the same input formats as SetHeaders. // Returns the request for method chaining. func (req *Request) AddHeaders(headers ...any) *Request { if req.request == nil || headers == nil { return req } req.applyHeaders(headers, func(h http.Header, k, v string) { h.Add(k, v) }) return req } // applyHeaders is a helper function that processes various header input formats and applies them to an HTTP request. // It handles type checking, conversion, and delegation to the provided setOrAdd function for actual header manipulation. // Supports ordered header maps for fingerprinting and maintains compatibility with multiple map and header types. func (req *Request) applyHeaders( rawHeaders []any, setOrAdd func(h http.Header, key, value string), ) { r := req.request if len(rawHeaders) >= 2 { var key, value string switch k := rawHeaders[0].(type) { case string: key = k case g.String: key = k.Std() default: panic(fmt.Sprintf("unsupported key type: expected 'string' or 'String', got %T", rawHeaders[0])) } switch v := rawHeaders[1].(type) { case string: value = v case g.String: value = v.Std() default: panic(fmt.Sprintf("unsupported value type: expected 'string' or 'String', got %T", rawHeaders[1])) } setOrAdd(r.Header, key, value) return } switch h := rawHeaders[0].(type) { case http.Header: for key, values := range h { for _, value := range values { setOrAdd(r.Header, key, value) } } case Headers: for key, values := range h { for _, value := range values { setOrAdd(r.Header, key, value) } } case map[string]string: for key, value := range h { setOrAdd(r.Header, key, value) } case map[g.String]g.String: for key, value := range h { setOrAdd(r.Header, key.Std(), value.Std()) } case g.Map[string, string]: for key, value := range h { setOrAdd(r.Header, key, value) } case g.Map[g.String, g.String]: for key, value := range h { setOrAdd(r.Header, key.Std(), value.Std()) } case g.MapOrd[string, string]: updated := updateRequestHeaderOrder(req, h) updated.Iter().ForEach(func(key, value string) { setOrAdd(r.Header, key, value) }) case g.MapOrd[g.String, g.String]: updated := updateRequestHeaderOrder(req, h) updated.Iter().ForEach(func(key, value g.String) { setOrAdd(r.Header, key.Std(), value.Std()) }) default: panic(fmt.Sprintf("unsupported headers type: expected 'http.Header', 'surf.Headers', 'map[~string]~string', 'Map[~string, ~string]', or 'MapOrd[~string, ~string]', got %T", rawHeaders[0])) } } // updateRequestHeaderOrder processes ordered headers for HTTP/2 and HTTP/3 fingerprinting. // It maintains the specific order of headers which is crucial for browser fingerprinting. // Separates regular headers from pseudo-headers (starting with ':') and sets the appropriate // header order keys for the transport layer to use. Returns a filtered map containing only // non-pseudo headers with non-empty values. func updateRequestHeaderOrder[T ~string](r *Request, h g.MapOrd[T, T]) g.MapOrd[T, T] { hclone := h.Clone() if r.cli.builder != nil { switch r.cli.builder.browser { case chromeBrowser: chrome.Headers(&hclone, r.request.Method) case firefoxBrowser: firefox.Headers(&hclone, r.request.Method) } } headersKeys := g.TransformSlice(hclone.Iter(). Keys(). Map(func(s T) T { return T(g.String(s).Lower()) }). Collect(), func(t T) string { return string(t) }) headers, pheaders := headersKeys.Iter().Partition(func(v string) bool { return v[0] != ':' }) if headers.NotEmpty() { r.request.Header[http.HeaderOrderKey] = headers } if pheaders.NotEmpty() { r.request.Header[http.PHeaderOrderKey] = pheaders } return hclone.Iter(). Filter(func(header, data T) bool { return header[0] != ':' && len(data) != 0 }). Collect() }