-
Notifications
You must be signed in to change notification settings - Fork 151
Expand file tree
/
Copy pathruntime.go
More file actions
542 lines (480 loc) · 17.8 KB
/
Copy pathruntime.go
File metadata and controls
542 lines (480 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package client
import (
"context"
"fmt"
"mime"
"net/http"
"net/http/httputil"
"strings"
"sync"
"time"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/client/internal/request"
"github.com/go-openapi/runtime/logger"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/runtime/server-middleware/mediatype"
"github.com/go-openapi/runtime/yamlpc"
"github.com/go-openapi/strfmt"
)
const (
schemeHTTP = "http"
schemeHTTPS = "https"
)
// DefaultTimeout the default request timeout.
var DefaultTimeout = 30 * time.Second
// Runtime represents an API client that uses the transport
// to make [http] requests based on a swagger specification.
type Runtime struct {
DefaultMediaType string
DefaultAuthentication runtime.ClientAuthInfoWriter
Consumers map[string]runtime.Consumer
Producers map[string]runtime.Producer
Transport http.RoundTripper
Jar http.CookieJar
// Spec *spec.Document
Host string
BasePath string
Formats strfmt.Registry
// Deprecated: prefer [runtime.ContextualTransport.SubmitContext] to pass the request context explicitly.
Context context.Context //nolint:containedctx // we precisely want this type to contain the request context
Debug bool
// Trace enables connection-level diagnostic output via
// [net/http/httptrace]. When true, the runtime narrates the
// connection lifecycle of every request through r.logger.Debugf:
// DNS, dial, TLS handshake, idle-pool reuse, request body
// transfer, time-to-first-byte, response body transfer, and a
// trailing per-request summary line.
//
// Trace is orthogonal to Debug: Debug dumps wire bytes (request
// and response headers and body), Trace narrates how the
// connection got there. Both may be enabled independently.
//
// Trace is not coupled to the SWAGGER_DEBUG / DEBUG environment
// variables: it defaults to false and is only enabled by
// explicit assignment.
//
// Trace is primarily intended as a problem-investigation tool
// (the local equivalent of curl -vvv), not an always-on tracer.
// For distributed-trace correlation, use the OpenTelemetry
// integration ([Runtime.WithOpenTelemetry]).
Trace bool
logger logger.Logger
// MatchSuffix enables RFC 6839 structured-syntax suffix tolerance
// for codec lookup. When true, a response with Content-Type
// "application/problem+json" finds the JSON consumer registered
// under "application/json"; with the default false, the lookup
// is strict and falls through to the "*/*" wildcard if present.
// See [mediatype.AllowSuffix] for the semantics.
MatchSuffix bool
clientOnce *sync.Once
client *http.Client
schemes []string
response ClientResponseFunc
}
var _ runtime.ContextualTransport = &Runtime{}
// New creates a new default runtime for a swagger api runtime.Client.
func New(host, basePath string, schemes []string) *Runtime {
var rt Runtime
rt.DefaultMediaType = runtime.JSONMime
// Enhancement proposal: https://github.com/go-openapi/runtime/issues/385
rt.Consumers = map[string]runtime.Consumer{
runtime.YAMLMime: yamlpc.YAMLConsumer(),
runtime.JSONMime: runtime.JSONConsumer(),
runtime.XMLMime: runtime.XMLConsumer(),
runtime.TextMime: runtime.TextConsumer(),
runtime.HTMLMime: runtime.TextConsumer(),
runtime.CSVMime: runtime.CSVConsumer(),
runtime.MultipartFormMime: runtime.ByteStreamConsumer(),
runtime.URLencodedFormMime: runtime.ByteStreamConsumer(),
runtime.DefaultMime: runtime.ByteStreamConsumer(),
}
rt.Producers = map[string]runtime.Producer{
runtime.YAMLMime: yamlpc.YAMLProducer(),
runtime.JSONMime: runtime.JSONProducer(),
runtime.XMLMime: runtime.XMLProducer(),
runtime.TextMime: runtime.TextProducer(),
runtime.HTMLMime: runtime.TextProducer(),
runtime.CSVMime: runtime.CSVProducer(),
runtime.DefaultMime: runtime.ByteStreamProducer(),
}
rt.Transport = http.DefaultTransport
rt.Jar = nil
rt.Host = host
rt.BasePath = basePath
rt.Context = context.Background()
rt.clientOnce = new(sync.Once)
if !strings.HasPrefix(rt.BasePath, "/") {
rt.BasePath = "/" + rt.BasePath
}
rt.Debug = logger.DebugEnabled()
rt.logger = logger.StandardLogger{}
rt.response = newResponse
if len(schemes) > 0 {
rt.schemes = schemes
}
return &rt
}
// NewWithClient allows you to create a new transport with a configured [http.Client].
func NewWithClient(host, basePath string, schemes []string, client *http.Client) *Runtime {
rt := New(host, basePath, schemes)
if client != nil {
rt.clientOnce.Do(func() {
rt.client = client
})
}
return rt
}
// EnableConnectionReuse drains the remaining body from a response
// so that go will reuse the TCP connections.
//
// This is not enabled by default because there are servers where
// the response never gets closed and that would make the code hang forever.
// So instead it's provided as a [http] client [middleware] that can be used to override
// any request.
func (r *Runtime) EnableConnectionReuse() {
if r.client == nil {
r.Transport = KeepAliveTransport(
transportOrDefault(r.Transport, http.DefaultTransport),
)
return
}
r.client.Transport = KeepAliveTransport(
transportOrDefault(r.client.Transport,
transportOrDefault(r.Transport, http.DefaultTransport),
),
)
}
// CreateHTTPRequestContext creates the requests and bind the parameters, but does not send it over the wire
// like [Runtime.SubmitContext].
//
// The [http.Request] is complete with authentication, headers and body (including streamed body) and ready for callers
// to submit it to a [http.Client] of their choice, then consume the [http.Response].
//
// Most users would simply use [Runtime.SubmitContext], which wraps all these operations in one call.
func (r *Runtime) CreateHTTPRequestContext(ctx context.Context, operation *runtime.ClientOperation) (req *http.Request, cancel context.CancelFunc, err error) {
req, cancel, err = r.createHTTPRequestContext(ctx, operation)
return
}
// CreateHttpRequest builds the [http.Request] for the given operation, using
// [context.Background] as the request context.
//
// Any per-operation timeout declared by the operation's [runtime.ClientRequestWriter]
// is silently ignored here, which can leak a context-cancellation channel if the
// caller relies on it.
//
// Deprecated: use [Runtime.CreateHTTPRequestContext] instead, with explicit
// control over the request context and its cancellation.
func (r *Runtime) CreateHttpRequest(operation *runtime.ClientOperation) (req *http.Request, err error) { //nolint:revive
req, _, err = r.createHTTPRequestContext(context.Background(), operation)
return
}
// Submit a request and when there is a body on success it will turn that into the result
// all other things are turned into an api error for swagger which retains the status code.
//
// This call inherits the context possibly put in the operation, otherwise the one possibly put in the [Runtime].
// If none are set, use [context.Background].
//
// Any timeout set by parameters is honored.
func (r *Runtime) Submit(operation *runtime.ClientOperation) (any, error) {
return r.SubmitContext(r.ensureContext(operation), operation)
}
// SubmitContext submits a request and returns the result.
//
// Errors are turned into an api error for swagger which retains the status code.
//
// Unlike [Submit], [SubmitContext] only injects the context provided by the caller:
// contexts possibly cached in operation or runtime are ignored.
//
// On the other hand, a timeout set by parameters is honored.
func (r *Runtime) SubmitContext(parentCtx context.Context, operation *runtime.ClientOperation) (any, error) {
req, cancel, err := r.createHTTPRequestContext(parentCtx, operation)
if err != nil {
return nil, err
}
defer cancel()
r.ensureClient()
if err := r.dumpRequest(req); err != nil {
return nil, err
}
// Attach the trace session before Do so the httptrace hooks
// fire during the round-trip. The session emits its trailing
// summary on finish; the response body is consumed by
// ReadResponse downstream, after which finish is called.
var trace *traceSession
if r.Trace {
trace = newTraceSession(r.logger, req.Method, req.URL.String(),
introspectTLSConfig(r.pickClient(operation)))
//nolint:contextcheck // We intentionally derive from req.Context() to layer the trace hooks onto the existing request context.
req = req.WithContext(trace.attach(req.Context()))
if req.Body != nil {
req.Body = trace.wrapRequestBody(req.Body)
}
defer trace.finish()
}
res, err := r.pickClient(operation).Do(req)
if err != nil {
if trace != nil {
trace.onRoundTripError(err)
}
return nil, err
}
defer res.Body.Close()
if trace != nil {
trace.onResponse(res.StatusCode)
res.Body = trace.wrapResponseBody(res.Body)
}
ct := res.Header.Get(runtime.HeaderContentType)
if ct == "" { // this should really never occur
ct = r.DefaultMediaType
}
if err := r.dumpResponse(res, ct); err != nil {
return nil, err
}
cons, err := r.resolveConsumer(ct)
if err != nil {
return nil, err
}
return operation.Reader.ReadResponse(r.response(res), cons)
}
// SetDebug changes the debug flag.
// It ensures that client and middlewares have the set debug level.
func (r *Runtime) SetDebug(debug bool) {
r.Debug = debug
middleware.Debug = debug
}
// SetLogger changes the logger stream.
// It ensures that client and middlewares use the same logger.
func (r *Runtime) SetLogger(logger logger.Logger) {
r.logger = logger
middleware.Logger = logger
}
type ClientResponseFunc = func(*http.Response) runtime.ClientResponse //nolint:revive
// SetResponseReader changes the response reader implementation.
func (r *Runtime) SetResponseReader(f ClientResponseFunc) {
if f == nil {
return
}
r.response = f
}
func (r *Runtime) ensureContext(operation *runtime.ClientOperation) context.Context {
switch {
case operation.Context != nil: //nolint:staticcheck // kept for backward compatibility
return operation.Context
case r.Context != nil:
return r.Context
default:
return context.Background()
}
}
func (r *Runtime) pickScheme(schemes []string) string {
if v := r.selectScheme(r.schemes); v != "" {
return v
}
if v := r.selectScheme(schemes); v != "" {
return v
}
return schemeHTTP
}
func (r *Runtime) selectScheme(schemes []string) string {
schLen := len(schemes)
if schLen == 0 {
return ""
}
scheme := schemes[0]
// prefer https, but skip when not possible
if scheme != schemeHTTPS && schLen > 1 {
for _, sch := range schemes {
if sch == schemeHTTPS {
scheme = sch
break
}
}
}
return scheme
}
func transportOrDefault(left, right http.RoundTripper) http.RoundTripper {
if left == nil {
return right
}
return left
}
// ensureClient lazily initializes r.client from r.Transport and r.Jar
// on first use. Safe under concurrent calls via sync.Once.
func (r *Runtime) ensureClient() {
r.clientOnce.Do(func() {
r.client = &http.Client{
Transport: r.Transport,
Jar: r.Jar,
}
})
}
// pickClient returns the http.Client to use for this operation: the
// per-operation override if set, else the runtime's shared client.
func (r *Runtime) pickClient(operation *runtime.ClientOperation) *http.Client {
if operation.Client != nil {
return operation.Client
}
return r.client
}
// dumpRequest writes the outgoing request to the debug logger when
// r.Debug is enabled. No-op otherwise. Returns the dump error so the
// caller can decide whether to abort the submit.
func (r *Runtime) dumpRequest(req *http.Request) error {
if !r.Debug {
return nil
}
b, err := httputil.DumpRequestOut(req, true)
if err != nil {
return err
}
r.logger.Debugf("%s\n", string(b))
return nil
}
// dumpResponse writes the incoming response to the debug logger when
// r.Debug is enabled. The body is omitted for runtime.DefaultMime
// (binary blob). No-op otherwise.
func (r *Runtime) dumpResponse(res *http.Response, ct string) error {
if !r.Debug {
return nil
}
printBody := ct != runtime.DefaultMime // Spare the terminal from a binary blob.
b, err := httputil.DumpResponse(res, printBody)
if err != nil {
return err
}
r.logger.Debugf("%s\n", string(b))
return nil
}
// resolveConsumer parses ct and returns the registered Consumer for
// that media type. Lookup is alias-aware (RFC 9512 §2.1 — yaml
// aliases) and, when [Runtime.MatchSuffix] is true, also tolerates
// RFC 6839 structured-syntax suffix media types (+json, +xml, +yaml).
// Falls back to the "*/*" entry if no match found.
func (r *Runtime) resolveConsumer(ct string) (runtime.Consumer, error) {
if _, _, err := mime.ParseMediaType(ct); err != nil {
return nil, fmt.Errorf("parse content type: %w", err)
}
if cons, ok := mediatype.Lookup(r.Consumers, ct, r.matchOpts()...); ok {
return cons, nil
}
if cons, ok := r.Consumers["*/*"]; ok {
return cons, nil
}
// scream about not knowing what to do
return nil, fmt.Errorf("no consumer: %q", ct)
}
// matchOpts builds the mediatype.MatchOption slice for codec
// lookups on the Runtime, currently just the AllowSuffix opt-in.
func (r *Runtime) matchOpts() []mediatype.MatchOption {
if !r.MatchSuffix {
return nil
}
return []mediatype.MatchOption{mediatype.AllowSuffix()}
}
// createHTTPRequestContext is the context-aware builder of a [http.Request].
//
// The returned [http.Request] carries a context derived from parentCtx that
// honors the per-request timeout set during WriteToRequest. Callers must
// invoke cancel once the response is fully read.
func (r *Runtime) createHTTPRequestContext(parentCtx context.Context, operation *runtime.ClientOperation) (*http.Request, context.CancelFunc, error) {
req, cmt, auth, err := r.prepareRequest(operation)
if err != nil {
return nil, nil, err
}
httpReq, cancel, err := req.BuildHTTPContext(parentCtx, cmt, r.BasePath, r.Producers, r.Formats, auth)
if err != nil {
return nil, nil, err
}
r.applyHostScheme(httpReq, operation)
return httpReq, cancel, nil
}
// prepareRequest performs the operation-to-request setup that is
// independent of how the http.Request is finally assembled: parameters,
// headers, default authentication, and consumes-media-type selection.
func (r *Runtime) prepareRequest(operation *runtime.ClientOperation) (*request.Request, string, runtime.ClientAuthInfoWriter, error) {
params, _, auth := operation.Params, operation.Reader, operation.AuthInfo
req := request.New(operation.Method, operation.PathPattern, params)
_ = req.SetTimeout(DefaultTimeout) // the timeout may be overridden by ClientRequestWriter
req.SetConsumes(operation.ConsumesMediaTypes)
accept := make([]string, 0, len(operation.ProducesMediaTypes))
accept = append(accept, operation.ProducesMediaTypes...)
if err := req.SetHeaderParam(runtime.HeaderAccept, accept...); err != nil {
return nil, "", nil, err
}
if auth == nil && r.DefaultAuthentication != nil {
auth = runtime.ClientAuthInfoWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error {
if req.GetHeaderParams().Get(runtime.HeaderAuthorization) != "" {
return nil
}
return r.DefaultAuthentication.AuthenticateRequest(req, reg)
})
}
cmt := pickConsumesMediaType(operation.ConsumesMediaTypes, r.Producers, r.DefaultMediaType, r.matchOpts()...)
if _, ok := mediatype.Lookup(r.Producers, cmt, r.matchOpts()...); !ok && cmt != runtime.MultipartFormMime && cmt != runtime.URLencodedFormMime {
return nil, "", nil, fmt.Errorf("none of producers: %v registered. try %s", r.Producers, cmt)
}
return req, cmt, auth, nil
}
// applyHostScheme stamps the runtime's host and the operation-selected
// scheme onto the freshly built http.Request.
func (r *Runtime) applyHostScheme(httpReq *http.Request, operation *runtime.ClientOperation) {
httpReq.URL.Scheme = r.pickScheme(operation.Schemes)
httpReq.URL.Host = r.Host
httpReq.Host = r.Host
}
// pickConsumesMediaType selects which Content-Type the client will send.
//
// Selection rules, in priority order:
//
// 1. multipart/form-data if any consumes entry advertises it (it streams
// and preserves per-file Content-Type, regardless of codegen ordering;
// resolves issue #286);
// 2. the first non-empty entry whose mime is either structural
// (multipart/form-data or application/x-www-form-urlencoded — these
// do not need a producer in the map) or has a producer registered in
// producers — this lets the client gracefully skip unregistered
// spec entries instead of erroring at the gate that follows;
// 3. the first non-empty entry overall (preserves the historical error
// path: the gate at the call site reports "none of producers" with
// the unregistered mime, so the diagnostic is unchanged when nothing
// in consumes is registered);
// 4. def, if consumes is empty or all empty strings.
//
// Step 2 closes part of issues #32 and #386: an operation declaring
// `consumes: [application/x-vendor, application/json]` with no vendor
// producer registered now silently uses JSON instead of erroring.
func pickConsumesMediaType(consumes []string, producers map[string]runtime.Producer, def string, opts ...mediatype.MatchOption) string {
for _, mt := range consumes {
if strings.EqualFold(mt, runtime.MultipartFormMime) {
return mt
}
}
var firstNonEmpty string
for _, mt := range consumes {
if mt == "" {
continue
}
if firstNonEmpty == "" {
firstNonEmpty = mt
}
if isStructuralMime(mt) {
return mt
}
if _, ok := mediatype.Lookup(producers, mt, opts...); ok {
return mt
}
}
if firstNonEmpty != "" {
return firstNonEmpty
}
return def
}
// isStructuralMime reports whether mt is a media type whose body shape
// is owned by the runtime (multipart envelope, urlencoded form). These
// do not require an entry in the producers map.
func isStructuralMime(mt string) bool {
return strings.EqualFold(mt, runtime.MultipartFormMime) ||
strings.EqualFold(mt, runtime.URLencodedFormMime)
}