package util import ( "fmt" "io" "net/http" "github.com/getsentry/sentry-go/internal/debuglog" "github.com/getsentry/sentry-go/internal/protocol" "github.com/getsentry/sentry-go/internal/ratelimit" ) // MaxDrainResponseBytes is the maximum number of bytes that transport // implementations will read from response bodies when draining them. const MaxDrainResponseBytes = 16 << 10 // HandleHTTPResponse is a helper method that reads the HTTP response and handles debug output. func HandleHTTPResponse(response *http.Response, identifier string) bool { if response.StatusCode >= 200 && response.StatusCode < 300 { return true } if response.StatusCode >= 400 && response.StatusCode <= 599 { body, err := io.ReadAll(io.LimitReader(response.Body, MaxDrainResponseBytes)) if err != nil { debuglog.Printf("Error while reading response body: %v", err) return false } switch { case response.StatusCode == http.StatusRequestEntityTooLarge: debuglog.Printf("Sending %s failed because the request was too large: %s", identifier, string(body)) case response.StatusCode >= 500: debuglog.Printf("Sending %s failed with server error %d: %s", identifier, response.StatusCode, string(body)) default: debuglog.Printf("Sending %s failed with client error %d: %s", identifier, response.StatusCode, string(body)) } return false } debuglog.Printf("Unexpected status code %d for event %s", response.StatusCode, identifier) return false } // EnvelopeIdentifier returns a human-readable identifier for the event to be used in log messages. // Format: " []". func EnvelopeIdentifier(envelope *protocol.Envelope) string { if envelope == nil || len(envelope.Items) == 0 { return "empty envelope" } var description string // we don't currently support mixed envelope types, so all event types would have the same type. itemType := envelope.Items[0].Header.Type switch itemType { case protocol.EnvelopeItemTypeEvent: description = "error" case protocol.EnvelopeItemTypeTransaction: description = "transaction" case protocol.EnvelopeItemTypeCheckIn: description = "check-in" case protocol.EnvelopeItemTypeLog: logCount := 0 for _, item := range envelope.Items { if item != nil && item.Header != nil && item.Header.Type == protocol.EnvelopeItemTypeLog && item.Header.ItemCount != nil { logCount += *item.Header.ItemCount } } description = fmt.Sprintf("%d log events", logCount) case protocol.EnvelopeItemTypeTraceMetric: metricCount := 0 for _, item := range envelope.Items { if item != nil && item.Header != nil && item.Header.Type == protocol.EnvelopeItemTypeTraceMetric && item.Header.ItemCount != nil { metricCount += *item.Header.ItemCount } } description = fmt.Sprintf("%d metric events", metricCount) default: description = fmt.Sprintf("%s event", itemType) } return fmt.Sprintf("%s [%s]", description, envelope.Header.EventID) } // SendResult holds the outcome of an HTTP request sent to Sentry. type SendResult struct { Success bool StatusCode int Limits ratelimit.Map } // IsSendError returns true if the response indicates a server/client error that should be recorded // as send_error for client report outcomes (non-429 failures). func (r *SendResult) IsSendError() bool { return !r.Success && r.StatusCode != http.StatusTooManyRequests } // DoSendRequest executes an HTTP request, handles response logging, extracts rate limits, and // drains+closes the response body. func DoSendRequest(client *http.Client, request *http.Request, identifier string) (*SendResult, error) { response, err := client.Do(request) //nolint:gosec // G704: request is constructed internally, not from user input if err != nil { return nil, err } success := HandleHTTPResponse(response, identifier) limits := ratelimit.FromResponse(response) // Drain body up to a limit and close it, allowing the transport to reuse TCP connections. _, _ = io.CopyN(io.Discard, response.Body, MaxDrainResponseBytes) response.Body.Close() return &SendResult{ Success: success, StatusCode: response.StatusCode, Limits: limits, }, nil }