package httpx import ( "errors" "fmt" "io" "mime" "net/http" "strings" apierrors "git.yuntuo.hk/k2safe/go-omni/errors" ) // Body borrows the limited request stream for the duration of a Logic call. // Raw contains the exact body bytes (never JSON-decoded). For multipart input, // consume Parts sequentially with NextPart and close each part. No temporary // files are created. Do not retain Body or its readers after Logic returns. type Body struct { Raw io.Reader Parts *MultipartReader Header http.Header } func NewBody(r *http.Request, mode string) (*Body, error) { if r == nil { return nil, errors.New("httpx: request required") } b := &Body{Raw: r.Body, Header: r.Header.Clone()} if b.Raw == nil { b.Raw = http.NoBody } switch mode { case "raw": case "multipart": media, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil || media != "multipart/form-data" || params["boundary"] == "" { return nil, apierrors.New(415, "UNSUPPORTED_MEDIA_TYPE", "multipart/form-data with a valid boundary required").WithCause(err) } input := &multipartInput{Reader: b.Raw, boundary: "--" + params["boundary"] + "--"} request := *r request.Body = io.NopCloser(input) parts, err := request.MultipartReader() if err != nil { return nil, apierrors.New(415, "UNSUPPORTED_MEDIA_TYPE", "multipart/form-data with a valid boundary required").WithCause(err) } b.Parts = &MultipartReader{reader: parts, input: input} default: return nil, fmt.Errorf("httpx: unsupported body mode %q", mode) } return b, nil } // Complete validates remaining multipart parts and checks unread bytes against // the route limit before committing a success response. Slow clients remain // bounded by the server's read timeout. Do not read Raw alongside Parts. func (b *Body) Complete() error { if b == nil { return nil } if b.Parts != nil { for { part, err := b.Parts.NextPart() if err == io.EOF { break } if err != nil { return err } if err = part.Close(); err != nil { return err } } } if b.Raw == nil { return nil } _, err := io.Copy(io.Discard, b.Raw) return err } // Stream transfers ownership of Content to the generated handler. The handler // closes it even on failure, and writes it without the JSON response envelope. // Name enables an attachment filename; leave it empty for raw callback output. // ContentType may refine the declared type with parameters (such as charset). type Stream struct { Content io.ReadCloser Name string ContentType string } func (s *Stream) Close() error { if s == nil || s.Content == nil { return nil } return s.Content.Close() } func (c *Context) WriteStream(status int, declaredType string, stream *Stream) (err error) { if stream == nil { return errors.New("httpx: nil stream response") } defer func() { err = errors.Join(err, stream.Close()) }() if stream.Content == nil { return errors.New("httpx: stream content required") } if c.Responded() { return errors.New("httpx: response already committed") } if status < 200 || status > 599 || status == 204 || status == 205 || status == 304 { return errors.New("httpx: stream requires body-bearing status") } contentType := stream.ContentType if contentType == "" { contentType = declaredType } media, _, parseErr := mime.ParseMediaType(contentType) if parseErr != nil || media != declaredType || strings.ContainsAny(contentType, "\r\n") { return errors.New("httpx: stream content type differs from API contract") } writer := c.Raw() writer.Header().Set("Content-Type", contentType) if stream.Name != "" { writer.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": stream.Name})) } writer.WriteHeader(status) _, err = io.Copy(writer, stream.Content) return err }