package requests import ( "bufio" "errors" "fmt" "io" "maps" "mime" "net/http" "slices" ) // DefaultValidator is the validator applied by Builder unless otherwise specified. var DefaultValidator ResponseHandler = CheckStatus( http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNonAuthoritativeInfo, http.StatusNoContent, ) // ResponseError is the error type produced by CheckStatus and CheckContentType. type ResponseError http.Response // Error fulfills the error interface. func (se *ResponseError) Error() string { return fmt.Sprintf("response error for %s", se.Request.URL.Redacted()) } // CheckStatus validates the response has an acceptable status code. func CheckStatus(acceptStatuses ...int) ResponseHandler { return func(res *http.Response) error { if slices.Contains(acceptStatuses, res.StatusCode) { return nil } return fmt.Errorf("%w: unexpected status: %d", (*ResponseError)(res), res.StatusCode) } } // HasStatusErr returns true if err is a ResponseError caused by any of the codes given. func HasStatusErr(err error, codes ...int) bool { if err == nil { return false } if se := new(ResponseError); errors.As(err, &se) { return slices.Contains(codes, se.StatusCode) } return false } // CheckContentType validates that a response has one of the given content type headers. func CheckContentType(cts ...string) ResponseHandler { return func(res *http.Response) error { mt, _, err := mime.ParseMediaType(res.Header.Get("Content-Type")) if err != nil { return fmt.Errorf("%w: problem matching Content-Type", (*ResponseError)(res)) } if slices.Contains(cts, mt) { return nil } return fmt.Errorf("%w: unexpected Content-Type: %s", (*ResponseError)(res), mt) } } type bufioCloser struct { *bufio.Reader io.Closer } // CheckPeek wraps the body of a response in a bufio.Reader and // gives f a peek at the first n bytes for validation. func CheckPeek(n int, f func([]byte) error) ResponseHandler { return func(res *http.Response) error { // ensure buffer is at least minimum size buf := bufio.NewReader(res.Body) // ensure large peeks will fit in the buffer buf = bufio.NewReaderSize(buf, n) res.Body = &bufioCloser{ buf, res.Body, } b, err := buf.Peek(n) if err != nil && err != io.EOF { return err } return f(b) } } // CopyHeaders copies the response headers to h. func CopyHeaders(h map[string][]string) ResponseHandler { return func(res *http.Response) error { maps.Copy(h, res.Header) return nil } }