Files
2026-06-12 13:43:37 +02:00

946 lines
30 KiB
Go

// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package request
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
)
var _ runtime.ClientRequest = new(Request) // ensure compliance to the interface
// Request represents a swagger client request.
// It binds parameters to a HTTP request.
//
// The main purpose of this struct is to hide the machinery of adding OpenAPI v2 parameters to a transport request.
//
// A generated client only implements what is necessary to turn a parameter into a valid value for these methods.
//
// There is no parameter validation here, it is assumed to be used after a spec has been validated.
//
// # Request binding
//
// The binding of parameters is carried out by method [Request.BuildHTTPContext].
//
// It analyzes parameters, which may come in different flavors:
//
// - a file or multipart form containing a file
// - a body which is a [io.Reader]
// - a buffered body (regular schema body, including urlencoded form)
//
// In all cases, we may also have query or path parameters encoded in the URL, or header parameters.
//
// The result is a [http.Request], with the following properties:
//
// - file, multipart form or [io.Reader] body: a streaming request with an attached go routine that consumes the [io.Reader].
// - buffered body: a simple request
//
// The caller passes the parent [context.Context] to [Request.BuildHTTPContext] and receives back a cancel
// function to release the resources held by the derived request context once the response is consumed.
//
// # Authentication
//
// Authentication is built in the request by using a [runtime.ClientAuthInfoWriter].
// This helper may need to inspect the body of the request before sending authentication info.
// To cover that case, streaming bodies use a copy of the body [io.Reader] for the [runtime.ClientAuthInfoWriter]
// to consume if it wants to.
//
// # Content negotiation
//
// The [Request] detects `multipart/form-data` to switch to streamed request.
//
// `application/x-www-form-urlencoded` is also honored, even for file parameters, which are not streamed in this case.
// File parameters default behavior is `multipart/form-data`.
//
// The natural way to define the `Content-Type` header is to use the `contentType` parameter to switch to the map of
// available body producers.
//
// For buffered requests, this setting override any `Content-Type` header possibly set by calling [Request.SetHeaderParam].
//
// For streamed requests, users may want more flexibility, as we enter custom territory, with use-cases not supported by OpenAPI v2.
//
// The `Content-Type` header of a streamed request is defined using the following sequence:
//
// 1. if the caller sets an explicit value already in header — the user set it via
// [Request.SetHeaderParam] during WriteToRequest, and we treat that as an intentional escape hatch
// 2. use payload's [runtime.ContentTyper] declaration (in this case, the produced payload knows its content type)
// 3. use `application/octet-stream` if it is available in the registered producers
// 4. otherwise set the picker's mediaType
//
// For multi-part requests, the content type of each part is auto-detected using the following sequence:
//
// 1. use [runtime.ContentTyper] declaration (in this case, the file payload knows its content type)
// 2. use [http.DetectContentType] on the first 512 bytes of the file
//
// # Concurrency
//
// A [Request] is a disposable object that is NOT intended to be reused or called concurrently.
//
// # Future evolutions
//
// There might be other similar structs that convert to other transports.
type Request struct {
pathPattern string
method string
writer runtime.ClientRequestWriter
pathParams map[string]string
header http.Header
query url.Values
formFields url.Values
fileFields map[string][]runtime.NamedReadCloser
payload any
// consumes carries the operation's full ConsumesMediaTypes list so
// that buildHTTP — which runs after the writer populates the payload
// — can apply payload-aware fallback rules (see streamFallbackMime).
//
// This is set by Runtime.createHttpRequest.
consumes []string
timeout time.Duration
buf *bytes.Buffer
getBody func(r *Request) []byte
}
// New creates a new http client [Request] to handle OpenAPI v2 parameters.
func New(method, pathPattern string, writer runtime.ClientRequestWriter) *Request {
return &Request{
pathPattern: pathPattern,
method: method,
writer: writer,
header: make(http.Header),
query: make(url.Values),
timeout: 0,
getBody: getRequestBuffer,
}
}
// GetMethod yields the method being used.
func (r *Request) GetMethod() string {
return r.method
}
// GetPath yields the URL path being used.
func (r *Request) GetPath() string {
pth := r.pathPattern
for k, v := range r.pathParams {
pth = strings.ReplaceAll(pth, "{"+k+"}", v)
}
return pth
}
// GetBody returns the request body, if any.
//
// For streaming requests, this is a copy of the original [io.Reader].
func (r *Request) GetBody() []byte {
return r.getBody(r)
}
// SetHeaderParam adds a header parameter to the request.
//
// The header key is always canonicalized.
//
// - when there is only 1 value provided, it will set it.
// - when there are several values provided, it will add all of those (no overriding).
func (r *Request) SetHeaderParam(name string, values ...string) error {
if r.header == nil {
r.header = make(http.Header)
}
r.header[http.CanonicalHeaderKey(name)] = values
return nil
}
// GetHeaderParams returns all headers currently set for the request.
func (r *Request) GetHeaderParams() http.Header {
return r.header
}
// SetQueryParam adds a query parameter to the request.
//
// - when there is only 1 value provided, it will set it.
// - when there are several values provided, it will add all of those (no overriding).
func (r *Request) SetQueryParam(name string, values ...string) error {
if r.query == nil {
r.query = make(url.Values)
}
r.query[name] = values
return nil
}
// GetQueryParams returns a copy of all query params currently set for the request.
func (r *Request) GetQueryParams() url.Values {
result := make(url.Values, len(r.query))
for key, values := range r.query {
result[key] = append([]string{}, values...)
}
return result
}
// SetFormParam adds a form param to the request.
//
// - when there is only 1 value provided, it will set it.
// - when there are several values provided, it will add all of those (no overriding).
func (r *Request) SetFormParam(name string, values ...string) error {
if r.formFields == nil {
r.formFields = make(url.Values)
}
r.formFields[name] = values
return nil
}
// SetPathParam adds a path param to the request.
func (r *Request) SetPathParam(name string, value string) error {
if r.pathParams == nil {
r.pathParams = make(map[string]string)
}
r.pathParams[name] = value
return nil
}
// SetFileParam adds a file parameter to the request.
//
// Files must implement [runtime.NamedReadCloser].
//
// [runtime.File] is proposed as the default concrete implementation.
func (r *Request) SetFileParam(name string, files ...runtime.NamedReadCloser) error {
for _, file := range files {
if actualFile, ok := file.(*os.File); ok {
fi, err := os.Stat(actualFile.Name())
if err != nil {
return err
}
if fi.IsDir() {
return fmt.Errorf("%q is a directory, only files are supported", file.Name())
}
}
}
if r.fileFields == nil {
r.fileFields = make(map[string][]runtime.NamedReadCloser)
}
if r.formFields == nil {
r.formFields = make(url.Values)
}
r.fileFields[name] = files
return nil
}
// GetFileParam yields all file parameters.
func (r *Request) GetFileParam() map[string][]runtime.NamedReadCloser {
return r.fileFields
}
// SetBodyParam sets a body parameter on the request.
//
// This does not yet serialize the object: actual serialization happens as late as possible.
func (r *Request) SetBodyParam(payload any) error {
r.payload = payload
return nil
}
// GetBodyParam returns the body payload.
func (r *Request) GetBodyParam() any {
return r.payload
}
// GetTimeout sets the timeout for a request.
func (r *Request) GetTimeout() time.Duration {
return r.timeout
}
// SetTimeout sets the timeout for a request.
func (r *Request) SetTimeout(timeout time.Duration) error {
r.timeout = timeout
return nil
}
// SetConsumes sets the list of registered consumed content for a request.
func (r *Request) SetConsumes(consumers []string) {
r.consumes = consumers
}
// BuildHTTPContext binds the request parameters and returns a ready-to-send [http.Request].
//
// Dispatch picks one of two end-to-end builders based on whether:
//
// - the body source is a stream (multipart pipe or stream payload)
// - or a buffer (urlencoded form, producer output, or no body)
//
// It starts by writing the request, then proceed with adding authentication,
// then finally assembling URL or header parameters.
//
// The split mirrors the auth question: streaming bodies require a lazy body-copy closure during [AuthenticateRequest],
// whereas buffered bodies do not.
//
// The returned [http.Request] carries a context derived from parentCtx that:
//
// - inherits any deadline or cancellation already set on parentCtx;
// - additionally honors the per-request timeout set via [Request.SetTimeout]
// (the [runtime.ClientRequestWriter] may override the runtime default during
// WriteToRequest, which is why the derivation happens here rather than
// at the call site).
//
// The returned cancel must be invoked by the caller (typically deferred)
// once the response has been fully read; otherwise resources held by the
// derived context — including any timeout timer — are leaked.
//
// On error the cancel is invoked internally and a no-op cancel is returned,
// so callers can defer cancel unconditionally.
func (r *Request) BuildHTTPContext(parentCtx context.Context, mediaType, basePath string,
producers map[string]runtime.Producer, registry strfmt.Registry, auth runtime.ClientAuthInfoWriter,
) (*http.Request, context.CancelFunc, error) {
if err := r.writer.WriteToRequest(r, registry); err != nil {
return nil, noop, err
}
ctx, cancel := deriveRequestContext(parentCtx, r.timeout)
r.buf = bytes.NewBuffer(nil)
var (
httpReq *http.Request
err error
)
if r.usesStreamingBody(mediaType) {
httpReq, err = r.buildStreamingRequest(ctx, mediaType, basePath, producers, registry, auth)
} else {
httpReq, err = r.buildBufferedRequest(ctx, mediaType, basePath, producers, registry, auth)
}
if err != nil {
cancel()
return nil, noop, err
}
return httpReq, cancel, nil
}
func noop() {}
// deriveRequestContext returns a child of parent bounded by timeout.
// If timeout == 0 the child is only canceled when the caller invokes
// cancel; any deadline already on parent is preserved. If timeout > 0
// the child uses the shortest of timeout and parent's existing deadline.
func deriveRequestContext(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
if timeout == 0 {
return context.WithCancel(parent)
}
return context.WithTimeout(parent, timeout)
}
// usesStreamingBody reports whether the request body must be assembled
// as a stream (an io.Pipe for multipart, or the payload's own reader
// for stream payloads).
//
// The complementary case is a fully buffered body in r.buf — urlencoded form, producer output, or no body at all.
func (r *Request) usesStreamingBody(mediaType string) bool {
if (len(r.formFields) > 0 || len(r.fileFields) > 0) && r.isMultipart(mediaType) {
return true
}
if r.payload != nil {
if _, ok := r.payload.(io.Reader); ok {
return true
}
}
return false
}
func (r *Request) isMultipart(mediaType string) bool {
// Strip media-type parameters before comparing: callers may legally
// pass `multipart/form-data; boundary=…` or
// `application/x-www-form-urlencoded; charset=utf-8` per RFC 7231,
// and a bare-string compare would route those to the wrong flow.
//
// mime.ParseMediaType lowercases the type/subtype and is
// case-insensitive on input, so plain == against our (lowercase)
// constants is sufficient on the happy path.
base, _, err := mime.ParseMediaType(mediaType)
if err != nil {
// Malformed mediaType: only the file-presence shortcut can
// fire — by definition we cannot recognize either canonical
// form mime in unparseable input.
return len(r.fileFields) > 0
}
// An explicit application/x-www-form-urlencoded choice is honored even when
// file fields are present: the spec allows files to travel as URL-encoded
// form values, although it does not stream and is discouraged. Without this
// short-circuit, picking urlencoded with files would silently fall back to
// multipart and emit an inconsistent Content-Type.
if base == runtime.URLencodedFormMime {
return false
}
if len(r.fileFields) > 0 {
return true
}
return base == runtime.MultipartFormMime
}
// buildBufferedRequest assembles a request whose body is fully
// buffered in r.buf before AuthenticateRequest runs — urlencoded form,
// producer-serialized payload, or no body.
//
// Auth is trivial in this flow because the buffer is already populated when the auth helper
// asks for the body via r.GetBody().
func (r *Request) buildBufferedRequest(ctx context.Context, mediaType, basePath string,
producers map[string]runtime.Producer, registry strfmt.Registry, auth runtime.ClientAuthInfoWriter,
) (*http.Request, error) {
var body io.Reader
var err error
switch {
case len(r.formFields) > 0 || len(r.fileFields) > 0:
body, err = r.writeURLEncodedBody(mediaType)
case r.payload != nil:
body, err = r.writeNonStreamPayload(mediaType, producers)
}
if err != nil {
return nil, err
}
if runtime.CanHaveBody(r.method) && body != nil && r.header.Get(runtime.HeaderContentType) == "" {
r.header.Set(runtime.HeaderContentType, mediaType)
}
if auth != nil {
if err := auth.AuthenticateRequest(r, registry); err != nil {
return nil, err
}
}
return r.assembleRequest(ctx, basePath, body)
}
// buildStreamingRequest assembles a request whose body is a stream —
// either an io.Pipe filled by the multipart goroutine, or the
// payload's own io.Reader.
//
// AuthenticateRequest consumes the body lazily through the getBody closure installed by
// applyAuthWithBodyCopy, which buffers the stream into r.buf so the http.Request can use the buffered copy.
//
// On any error path before the http.Request takes ownership of body, we close the body to release
// the underlying resource.
//
// For multipart this unblocks the spawned writer goroutine
// (it would otherwise park forever on pw.Write with no reader).
//
// For stream payloads it closes the user-provided io.ReadCloser.
func (r *Request) buildStreamingRequest(ctx context.Context, mediaType, basePath string,
producers map[string]runtime.Producer, registry strfmt.Registry, auth runtime.ClientAuthInfoWriter,
) (req *http.Request, retErr error) {
var body io.Reader
if len(r.formFields) > 0 || len(r.fileFields) > 0 {
body = r.writeMultipartBody(ctx, mediaType)
} else {
body = r.writeStreamPayload(mediaType, producers)
}
defer func() {
if retErr == nil {
return
}
if c, ok := body.(io.Closer); ok {
_ = c.Close()
}
}()
if runtime.CanHaveBody(r.method) && body != nil && r.header.Get(runtime.HeaderContentType) == "" {
r.header.Set(runtime.HeaderContentType, mediaType)
}
body, err := r.applyAuthWithBodyCopy(auth, body, registry)
if err != nil {
return nil, err
}
return r.assembleRequest(ctx, basePath, body)
}
// assembleRequest is the shared tail of both flows: build the URL
// path, create the http.Request, merge static query parameters, and
// finalize headers/query.
func (r *Request) assembleRequest(ctx context.Context, basePath string, body io.Reader) (*http.Request, error) {
urlPath, staticQueryParams, err := r.resolveURLPath(basePath)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, r.method, urlPath, body)
if err != nil {
return nil, err
}
if err := r.mergeStaticQuery(staticQueryParams); err != nil {
return nil, err
}
req.URL.RawQuery = r.query.Encode()
req.Header = r.header
return req, nil
}
// resolveURLPath builds the final url path string and returns the static
// query parameters extracted from basePath and r.pathPattern.
//
// Static query parameters from the path pattern take precedence over those
// from the base path; merging with r.query is the caller's responsibility
// (see [request.mergeStaticQuery]).
//
// The path is assembled from basePath + pathPattern with path-param
// substitution and trailing-slash preservation when the original
// pathPattern carried one.
func (r *Request) resolveURLPath(basePath string) (string, url.Values, error) {
basePathURL, err := url.Parse(basePath)
if err != nil {
return "", nil, err
}
staticQueryParams := basePathURL.Query()
pathPatternURL, err := url.Parse(r.pathPattern)
if err != nil {
return "", nil, err
}
for name, values := range pathPatternURL.Query() {
if _, present := staticQueryParams[name]; present {
staticQueryParams.Del(name)
}
for _, value := range values {
staticQueryParams.Add(name, value)
}
}
// path.Join strips trailing slashes; reinstate one whenever the
// pathPattern carried it, including the bare-root case ("/" under a
// non-empty basePath, which path.Join would collapse to "/basepath").
// The HasSuffix check on urlPath keeps the rewrite idempotent and
// avoids producing "//" when basePath is "/" or empty.
reinstateSlash := strings.HasSuffix(pathPatternURL.Path, "/")
urlPath := path.Join(basePathURL.Path, pathPatternURL.Path)
for k, v := range r.pathParams {
urlPath = strings.ReplaceAll(urlPath, "{"+k+"}", url.PathEscape(v))
}
if reinstateSlash && !strings.HasSuffix(urlPath, "/") {
urlPath += "/"
}
return urlPath, staticQueryParams, nil
}
// applyAuthWithBodyCopy runs auth.AuthenticateRequest for the
// streaming flow, where the http.Request body is a pipe or a payload
// reader rather than r.buf. If AuthenticateRequest asks for the body
// via r.GetBody(), the lazy closure copies the stream into r.buf on
// demand and reassigns body to r.buf so the post-auth source passed
// to http.NewRequestWithContext is the buffered copy.
//
// The closure is registered lazily because there is no way to know
// ahead of time whether AuthenticateRequest will read the body.
//
// On error precedence: a copy error is reported in preference to the
// AuthenticateRequest error, because a mis-read body may have
// interfered with auth.
//
// No-op when auth is nil; returns body unchanged.
func (r *Request) applyAuthWithBodyCopy(auth runtime.ClientAuthInfoWriter, body io.Reader, registry strfmt.Registry) (io.Reader, error) {
if auth == nil {
return body, nil
}
var copyErr error
var copied bool
r.getBody = func(r *Request) []byte {
if copied {
return getRequestBuffer(r)
}
defer func() {
copied = true
}()
if _, copyErr = io.Copy(r.buf, body); copyErr != nil {
return nil
}
if closer, ok := body.(io.ReadCloser); ok {
if copyErr = closer.Close(); copyErr != nil {
return nil
}
}
body = r.buf
return getRequestBuffer(r)
}
authErr := auth.AuthenticateRequest(r, registry)
// On error we return body alongside the error so the caller's
// cleanup defer (in buildStreamingRequest) can close the
// underlying pipe/stream. Caller treats body as ignorable when
// err != nil per Go convention; the defer reads it via closure.
if copyErr != nil {
return body, fmt.Errorf("error copying the request body: %w", copyErr)
}
if authErr != nil {
return body, authErr
}
return body, nil
}
// mergeStaticQuery overlays staticQuery onto r.query. On conflict r.query
// wins — the parameters set by the client take precedence over the ones
// extracted from basePath / pathPattern.
func (r *Request) mergeStaticQuery(staticQuery url.Values) error {
originalParams := r.GetQueryParams()
for k, v := range staticQuery {
if _, present := originalParams[k]; present {
continue
}
if err := r.SetQueryParam(k, v...); err != nil {
return err
}
}
return nil
}
// writeURLEncodedBody serializes form fields (and any file fields, per
// Swagger 2.0 fallback semantics) into r.buf as
// application/x-www-form-urlencoded. Sets Content-Type to mediaType and
// returns r.buf as the body source.
//
// Per Swagger 2.0, file form parameters can be sent under
// application/x-www-form-urlencoded by including the file content as a
// regular form-field value. The whole form is then percent-encoded as
// usual. This buffers the entire payload and does not preserve a
// per-file Content-Type — multipart/form-data is preferred when both
// are advertised by the operation.
func (r *Request) writeURLEncodedBody(mediaType string) (io.Reader, error) {
r.header.Set(runtime.HeaderContentType, mediaType)
values := url.Values{}
for k, vs := range r.formFields {
values[k] = append(values[k], vs...)
}
for fn, ff := range r.fileFields {
for _, fi := range ff {
data, ferr := io.ReadAll(fi)
if cerr := fi.Close(); cerr != nil && ferr == nil {
ferr = cerr
}
if ferr != nil {
return nil, ferr
}
values.Add(fn, string(data))
}
}
r.buf.WriteString(values.Encode())
return r.buf, nil
}
// writeMultipartBody assembles a multipart/form-data body via an
// io.Pipe. A goroutine streams form fields and files into the pipe
// writer; the pipe reader is returned as the body. Sets Content-Type to
// the multipart media type with the writer's boundary parameter.
//
// The goroutine owns the pipe writer's lifecycle: it closes the
// multipart writer (flushing the closing boundary) and the pipe writer
// when it finishes or hits an error.
func (r *Request) writeMultipartBody(ctx context.Context, mediaType string) io.Reader {
pr, pw := io.Pipe()
mp := multipart.NewWriter(pw)
r.header.Set(runtime.HeaderContentType, mangleContentType(mediaType, mp.Boundary()))
go r.streamMultipartParts(ctx, mp, pw)
return pr
}
// streamMultipartParts writes form fields then file fields to mp,
// closing mp and pw when done.
//
// Errors are reported by closing pw with the error so the consumer of pr observes them on its next Read.
//
// Context cancellation is observed at iteration boundaries (between
// fields and between files) and during file copy via a context-aware
// reader. When ctx is canceled the pipe writer is closed with ctx.Err()
// so the body consumer surfaces the cancellation as the read error.
func (r *Request) streamMultipartParts(ctx context.Context, mp *multipart.Writer, pw *io.PipeWriter) {
defer func() {
mp.Close()
pw.Close()
}()
for fn, v := range r.formFields {
for _, vi := range v {
if err := ctx.Err(); err != nil {
_ = pw.CloseWithError(err)
return
}
if err := mp.WriteField(fn, vi); err != nil {
logClose(err, pw)
return
}
}
}
defer func() {
for _, ff := range r.fileFields {
for _, ffi := range ff {
ffi.Close()
}
}
}()
for fn, f := range r.fileFields {
for _, fi := range f {
if err := ctx.Err(); err != nil {
_ = pw.CloseWithError(err)
return
}
var fileContentType string
if p, ok := fi.(runtime.ContentTyper); ok {
fileContentType = p.ContentType()
} else {
// Need to read the data so that we can detect the content type
const contentTypeBufferSize = 512
buf := make([]byte, contentTypeBufferSize)
size, err := fi.Read(buf)
if err != nil && !errors.Is(err, io.EOF) {
logClose(err, pw)
return
}
fileContentType = http.DetectContentType(buf)
fi = runtime.NamedReader(fi.Name(), io.MultiReader(bytes.NewReader(buf[:size]), fi))
}
// Create the MIME headers for the new part
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
escapeQuotes(fn), escapeQuotes(filepath.Base(fi.Name()))))
h.Set("Content-Type", fileContentType)
wrtr, err := mp.CreatePart(h)
if err != nil {
logClose(err, pw)
return
}
if _, err := io.Copy(wrtr, &ctxReader{ctx: ctx, r: fi}); err != nil {
logClose(err, pw)
return
}
}
}
}
// ctxReader wraps an [io.Reader] with a context check on each Read. Once
// ctx is done, subsequent Reads return ctx.Err() instead of delegating
// to the underlying reader. It does not preempt a Read already in flight
// — that is the source's responsibility (e.g. *os.File honors Close from
// another goroutine, network sources honor SetDeadline).
type ctxReader struct {
ctx context.Context //nolint:containedctx // io.Reader's Read method has no ctx parameter, so the wrapper must carry it on the struct
r io.Reader
}
func (cr *ctxReader) Read(p []byte) (int, error) {
if err := cr.ctx.Err(); err != nil {
return 0, err
}
return cr.r.Read(p)
}
// writeStreamPayload handles a stream payload (io.Reader /
// io.ReadCloser). The bytes flow through verbatim — no producer is
// invoked. The wire Content-Type is resolved via setStreamContentType
// (priority: existing header, payload's ContentTyper,
// streamFallbackMime, mediaType).
//
// Caller must ensure r.payload satisfies io.Reader (see
// [request.usesStreamingBody]).
func (r *Request) writeStreamPayload(mediaType string, producers map[string]runtime.Producer) io.Reader {
setStreamContentType(r.header, r.payload, mediaType, r.consumes, producers)
if rdr, ok := r.payload.(io.ReadCloser); ok {
return rdr
}
rdr, ok := r.payload.(io.Reader)
if !ok {
panic("internal error: payload expected to be an io.Reader") // guaranteed by earlier checks
}
return rdr
}
// writeNonStreamPayload runs the producer registered for mediaType
// against r.payload, writing into r.buf. The Content-Type header
// reflects the picker.
//
// SetHeaderParam("Content-Type", …) is intentionally NOT honored on
// the producer path because the producer is dispatched off mediaType —
// the wire header would otherwise misrepresent the body.
//
// The same reasoning applies to the form/multipart branch.
func (r *Request) writeNonStreamPayload(mediaType string, producers map[string]runtime.Producer) (io.Reader, error) {
r.header.Set(runtime.HeaderContentType, mediaType)
producer, ok := producers[mediaType]
if !ok {
return nil, fmt.Errorf("no producer registered for content type %q (register one with Runtime.Producers)", mediaType)
}
if err := producer.Produce(r.buf, r.payload); err != nil {
return nil, err
}
return r.buf, nil
}
var quoter = strings.NewReplacer(
"\\", "\\\\",
`"`, "\\\"",
"\r", "_",
"\n", "_",
)
// escapeQuotes escapes backslash and double-quote for embedding in a
// quoted-string Content-Disposition parameter value, and rewrites
// CR / LF to '_' to prevent header-injection through attacker-influenced
// field names or filenames.
//
// RFC 7578 §4.2 limits parameter values to printable characters; this
// is the conservative subset relevant to security (control characters
// that would split the header line into a forged header or part).
// Mirrors the known stdlib gap golang/go#19038.
func escapeQuotes(s string) string {
return quoter.Replace(s)
}
// setStreamContentType resolves and writes the wire Content-Type for a
// stream payload (io.Reader / io.ReadCloser). Priority:
//
// 1. an explicit value already in header — the user set it via
// SetHeaderParam during [ClientRequestWriter.WriteToRequest], and we treat that as an
// intentional escape hatch;
// 2. payload's [runtime.ContentTyper] declaration;
// 3. [streamFallbackMime] (Stage-2 octet-stream upgrade);
// 4. the picker's mediaType (passed in as the chain's terminal
// fallback).
//
// Does not apply to non-stream payloads or to form/multipart bodies —
// see the comment above the call site in [request.buildHTTP].
func setStreamContentType(
header http.Header,
payload any,
mediaType string,
candidates []string,
producers map[string]runtime.Producer,
) {
if header.Get(runtime.HeaderContentType) != "" {
return
}
fallback := streamFallbackMime(mediaType, candidates, producers)
header.Set(runtime.HeaderContentType, payloadContentType(payload, fallback))
}
// payloadContentType returns the payload's declared content type when
// it implements [runtime.ContentTyper] with a non-empty result, and
// fallback otherwise. Mirrors the per-file convention already used for
// multipart upload parts (see [request.buildHTTP] file-fields branch).
func payloadContentType(payload any, fallback string) string {
if t, ok := payload.(runtime.ContentTyper); ok {
if ct := t.ContentType(); ct != "" {
return ct
}
}
return fallback
}
// streamFallbackMime selects a wire content-type for a stream payload
// (io.Reader / io.ReadCloser) that has neither implemented
// `ContentType() string` nor declared an explicit value.
//
// The picker (Stage 1) ran without seeing the payload, so its choice
// may be wildly wrong for raw bytes — e.g. picking application/json
// for a payload that is just a stream of opaque data. When the
// candidate consumes list also offers application/octet-stream and
// the runtime has an octet-stream producer registered, that's a
// safer wire type than the picker's choice: it advertises "raw bytes"
// rather than making a structural claim about the body.
//
// If octet-stream is unavailable in either the candidate list or the
// producer set, the picker's choice is preserved. The wire header
// then continues to misrepresent the body — but no correct
// alternative exists and we cannot infer one without more
// information from the caller.
func streamFallbackMime(picked string, candidates []string, producers map[string]runtime.Producer) string {
if strings.EqualFold(picked, runtime.DefaultMime) {
return picked
}
for _, c := range candidates {
if strings.EqualFold(c, runtime.DefaultMime) {
if _, ok := producers[runtime.DefaultMime]; ok {
return runtime.DefaultMime
}
}
}
return picked
}
func getRequestBuffer(r *Request) []byte {
if r.buf == nil {
return nil
}
return r.buf.Bytes()
}
func logClose(err error, pw *io.PipeWriter) {
log.Println(err)
closeErr := pw.CloseWithError(err)
if closeErr != nil {
log.Println(closeErr)
}
}
func mangleContentType(mediaType, boundary string) string {
_ = mediaType // reserved for future enhancement: honor caller-provided media type
// Proposal for enhancement: honor caller's boundary if specified
return "multipart/form-data; boundary=" + boundary
}