129 lines
3.8 KiB
Go
129 lines
3.8 KiB
Go
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
|
|
}
|