Files
go-omni/httpx/transfer.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
}