Skip to content

Commit c1e2e2c

Browse files
authored
Merge pull request #469 from fredbi/fix/3113-file-urlencoded
Fix/3113 file urlencoded
2 parents 63e2d29 + 584ff89 commit c1e2e2c

14 files changed

Lines changed: 637 additions & 56 deletions

‎.codecov.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
codecov:
2+
notify:
3+
after_n_builds: 2
4+
5+
coverage:
6+
status:
7+
patch:
8+
default:
9+
target: 80%
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: unsafe-skipauth test
2+
3+
permissions:
4+
pull-requests: read
5+
contents: read
6+
7+
on:
8+
push:
9+
branches:
10+
- master
11+
12+
pull_request:
13+
14+
jobs:
15+
unsafe-skipauth-test:
16+
runs-on: ubuntu-latest
17+
18+
strategy:
19+
matrix:
20+
go-version: [stable, oldstable]
21+
22+
steps:
23+
-
24+
name: Checkout
25+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
26+
-
27+
name: Setup Go
28+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
29+
with:
30+
go-version: ${{ matrix.go-version }}
31+
-
32+
name: Test the dev-mode SetSkipAuth path
33+
run: >
34+
go test -tags openapi_unsafe_skipauth -race -count=1
35+
-coverprofile='skipauth.coverage.ubuntu-latest-${{ matrix.go-version }}.out'
36+
-covermode=atomic
37+
-coverpkg='github.com/go-openapi/runtime/middleware'
38+
./middleware/...
39+
-
40+
name: Upload coverage artifacts
41+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
42+
with:
43+
path: 'skipauth.coverage.ubuntu-latest-${{ matrix.go-version }}.out'
44+
name: 'skipauth.coverage.ubuntu-latest-${{ matrix.go-version }}'
45+
retention-days: 1
46+
47+
collect-coverage:
48+
needs: [unsafe-skipauth-test]
49+
if: ${{ !cancelled() && needs.unsafe-skipauth-test.result == 'success' }}
50+
uses: go-openapi/ci-workflows/.github/workflows/collect-coverage.yml@7982843ba86a9b47f456a44ed94e82f882b50a8d # v0.2.17
51+
secrets: inherit

‎client/mock_test.go‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,32 @@ func (m *mockRuntime) Submit(operation *runtime.ClientOperation) (any, error) {
2222
return map[string]any{}, nil
2323
}
2424

25+
// mockContextualRuntime satisfies [ContextualTransport] and records
26+
// which entry point a caller took plus the context observed at each.
27+
// Used to verify that wrappers prefer SubmitContext when available
28+
// and fall back to Submit otherwise.
29+
type mockContextualRuntime struct {
30+
mockRuntime
31+
32+
submitCalls int
33+
submitContextCalls int
34+
lastSubmitCtx context.Context //nolint:containedctx // test-only inspection of the ctx forwarded by the wrapper
35+
lastOpCtx context.Context //nolint:containedctx // test-only inspection of op.Context as seen by the wrapped transport
36+
}
37+
38+
func (m *mockContextualRuntime) Submit(operation *runtime.ClientOperation) (any, error) {
39+
m.submitCalls++
40+
m.lastOpCtx = operation.Context
41+
return m.mockRuntime.Submit(operation)
42+
}
43+
44+
func (m *mockContextualRuntime) SubmitContext(ctx context.Context, operation *runtime.ClientOperation) (any, error) {
45+
m.submitContextCalls++
46+
m.lastSubmitCtx = ctx
47+
m.lastOpCtx = operation.Context
48+
return m.mockRuntime.Submit(operation)
49+
}
50+
2551
type tres struct {
2652
}
2753

‎client/opentelemetry.go‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package client
55

66
import (
7+
"context"
78
"fmt"
89
"net/http"
910
"strings"
@@ -26,9 +27,14 @@ const (
2627

2728
// WithOpenTelemetry adds opentelemetry support to the provided runtime.
2829
// A new client span is created for each request.
29-
// If the context of the client operation does not contain an active span, no span is created.
3030
// The provided opts are applied to each spans - for example to add global tags.
31-
func (r *Runtime) WithOpenTelemetry(opts ...OpenTelemetryOpt) runtime.ClientTransport {
31+
//
32+
// The returned transport satisfies [ContextualTransport]: callers
33+
// should prefer [openTelemetryTransport.SubmitContext] over the
34+
// legacy [runtime.ClientOperation.Context] field. Setting that
35+
// field is still honored on the [openTelemetryTransport.Submit]
36+
// compatibility path.
37+
func (r *Runtime) WithOpenTelemetry(opts ...OpenTelemetryOpt) ContextualTransport {
3238
return newOpenTelemetryTransport(r, r.Host, opts)
3339
}
3440

@@ -52,7 +58,7 @@ func (r *Runtime) WithOpenTelemetry(opts ...OpenTelemetryOpt) runtime.ClientTran
5258
// usual opentracing options and opentracing-enabled transport.
5359
//
5460
// Passed options are ignored unless they are of type [OpenTelemetryOpt].
55-
func (r *Runtime) WithOpenTracing(opts ...any) runtime.ClientTransport {
61+
func (r *Runtime) WithOpenTracing(opts ...any) ContextualTransport {
5662
otelOpts := make([]OpenTelemetryOpt, 0, len(opts))
5763
for _, o := range opts {
5864
otelOpt, ok := o.(OpenTelemetryOpt)
@@ -155,11 +161,31 @@ func newOpenTelemetryTransport(transport runtime.ClientTransport, host string, o
155161
return tr
156162
}
157163

164+
// Submit implements [runtime.ClientTransport]. It honors the legacy
165+
// [runtime.ClientOperation.Context] field for backward compatibility
166+
// — that field is being phased out; new code should call
167+
// [openTelemetryTransport.SubmitContext] directly with an explicit
168+
// context.
158169
func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (any, error) {
159-
if op.Context == nil {
160-
return t.transport.Submit(op)
170+
ctx := op.Context
171+
if ctx == nil {
172+
ctx = context.Background()
161173
}
174+
return t.SubmitContext(ctx, op)
175+
}
162176

177+
// SubmitContext submits an operation with an explicit context that
178+
// drives both the tracing span and (when supported) the wrapped
179+
// transport's SubmitContext call. The legacy
180+
// [runtime.ClientOperation.Context] field is not consulted.
181+
//
182+
// When the wrapped transport implements [ContextualTransport], ctx is
183+
// forwarded directly via its SubmitContext. Otherwise, the legacy
184+
// Submit path is used: ctx is stamped onto op.Context for the
185+
// duration of that call and restored afterwards, so the wrapped
186+
// transport still receives a usable context. The legacy fallback
187+
// disappears once SubmitContext is universal (v2).
188+
func (t *openTelemetryTransport) SubmitContext(ctx context.Context, op *runtime.ClientOperation) (any, error) {
163189
params := op.Params
164190
reader := op.Reader
165191

@@ -171,7 +197,7 @@ func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (any, error
171197
}()
172198

173199
op.Params = runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, reg strfmt.Registry) error {
174-
span = t.newOpenTelemetrySpan(op, req.GetHeaderParams())
200+
span = t.newOpenTelemetrySpan(ctx, op, req.GetHeaderParams())
175201
return params.WriteToRequest(req, reg)
176202
})
177203

@@ -191,7 +217,7 @@ func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (any, error
191217
return reader.ReadResponse(response, consumer)
192218
})
193219

194-
submit, err := t.transport.Submit(op)
220+
submit, err := t.submitWrapped(ctx, op)
195221
if err != nil && span != nil {
196222
span.RecordError(err)
197223
span.SetStatus(codes.Error, err.Error())
@@ -200,9 +226,18 @@ func (t *openTelemetryTransport) Submit(op *runtime.ClientOperation) (any, error
200226
return submit, err
201227
}
202228

203-
func (t *openTelemetryTransport) newOpenTelemetrySpan(op *runtime.ClientOperation, header http.Header) trace.Span {
204-
ctx := op.Context
229+
//nolint:contextcheck // ctx is forwarded verbatim; the legacy Submit branch only stamps it onto op.Context for the wrapped transport.
230+
func (t *openTelemetryTransport) submitWrapped(ctx context.Context, op *runtime.ClientOperation) (any, error) {
231+
if sc, ok := t.transport.(ContextualTransport); ok {
232+
return sc.SubmitContext(ctx, op)
233+
}
234+
prev := op.Context
235+
op.Context = ctx
236+
defer func() { op.Context = prev }()
237+
return t.transport.Submit(op)
238+
}
205239

240+
func (t *openTelemetryTransport) newOpenTelemetrySpan(ctx context.Context, op *runtime.ClientOperation, header http.Header) trace.Span {
206241
tracer := t.tracer
207242
if tracer == nil {
208243
if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {

‎client/opentelemetry_test.go‎

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ func Test_OpenTelemetryRuntime_submit_nilAuthInfo(t *testing.T) {
6262
}
6363

6464
func Test_OpenTelemetryRuntime_submit_nilContext(t *testing.T) {
65+
// With op.Context nil, Submit now defaults to context.Background()
66+
// and still produces a span (under the modern SubmitContext path).
67+
// Previously this case skipped tracing entirely.
6568
exporter := tracetest.NewInMemoryExporter()
6669

6770
tp := tracesdk.NewTracerProvider(
@@ -77,7 +80,7 @@ func Test_OpenTelemetryRuntime_submit_nilContext(t *testing.T) {
7780
operation := testOperation(ctx)
7881
operation.Context = nil
7982

80-
assertOpenTelemetrySubmit(t, operation, exporter, 0) // just don't panic
83+
assertOpenTelemetrySubmit(t, operation, exporter, 1)
8184
}
8285

8386
func Test_injectOpenTelemetrySpanContext(t *testing.T) {
@@ -106,6 +109,72 @@ func Test_injectOpenTelemetrySpanContext(t *testing.T) {
106109
assert.Len(t, header, 1)
107110
}
108111

112+
// Test_OpenTelemetryRuntime_submitContext exercises the modern
113+
// SubmitContext entry point with a wrapped transport that satisfies
114+
// [ContextualTransport]. The wrapper must forward the explicit ctx
115+
// via the wrapped transport's SubmitContext (not via the legacy
116+
// op.Context field).
117+
func Test_OpenTelemetryRuntime_submitContext(t *testing.T) {
118+
exporter := tracetest.NewInMemoryExporter()
119+
tp := tracesdk.NewTracerProvider(
120+
tracesdk.WithSampler(tracesdk.AlwaysSample()),
121+
tracesdk.WithSyncer(exporter),
122+
)
123+
otel.SetTracerProvider(tp)
124+
125+
tracer := tp.Tracer("go-runtime")
126+
ctx, span := tracer.Start(context.Background(), "op")
127+
defer span.End()
128+
129+
header := map[string][]string{}
130+
mock := &mockContextualRuntime{mockRuntime: mockRuntime{req: runtime.TestClientRequest{Headers: header}}}
131+
tr := newOpenTelemetryTransport(mock, "remote_host", nil)
132+
133+
// Sentinel in op.Context: the SubmitContext path must ignore
134+
// it (no read for tracing, no mutation) and forward `ctx` instead.
135+
type sentinelKey struct{}
136+
staleCtx := context.WithValue(context.Background(), sentinelKey{}, "stale")
137+
op := testOperation(staleCtx)
138+
_, err := tr.SubmitContext(ctx, op)
139+
require.NoError(t, err)
140+
141+
assert.EqualT(t, 1, mock.submitContextCalls, "wrapper must call SubmitContext on a ContextualTransport")
142+
assert.EqualT(t, 0, mock.submitCalls, "legacy Submit must not be called when SubmitContext is available")
143+
assert.Equal(t, ctx, mock.lastSubmitCtx, "ctx must be forwarded verbatim")
144+
assert.Equal(t, staleCtx, mock.lastOpCtx, "op.Context must not be mutated under the SubmitContext path")
145+
assert.Len(t, exporter.GetSpans(), 1)
146+
}
147+
148+
// Test_OpenTelemetryRuntime_submitContext_legacyFallback verifies the
149+
// fallback path when the wrapped transport implements only the legacy
150+
// Submit method: the wrapper stamps ctx onto op.Context for the call
151+
// and restores the prior value afterwards.
152+
func Test_OpenTelemetryRuntime_submitContext_legacyFallback(t *testing.T) {
153+
exporter := tracetest.NewInMemoryExporter()
154+
tp := tracesdk.NewTracerProvider(
155+
tracesdk.WithSampler(tracesdk.AlwaysSample()),
156+
tracesdk.WithSyncer(exporter),
157+
)
158+
otel.SetTracerProvider(tp)
159+
160+
tracer := tp.Tracer("go-runtime")
161+
ctx, span := tracer.Start(context.Background(), "op")
162+
defer span.End()
163+
164+
header := map[string][]string{}
165+
mock := &mockRuntime{req: runtime.TestClientRequest{Headers: header}}
166+
tr := newOpenTelemetryTransport(mock, "remote_host", nil)
167+
168+
prevCtx, prevSpan := tracer.Start(context.Background(), "prev")
169+
defer prevSpan.End()
170+
op := testOperation(prevCtx)
171+
_, err := tr.SubmitContext(ctx, op)
172+
require.NoError(t, err)
173+
174+
assert.Equal(t, prevCtx, op.Context, "op.Context must be restored after the wrapped Submit returns")
175+
assert.Len(t, exporter.GetSpans(), 1)
176+
}
177+
109178
func assertOpenTelemetrySubmit(t *testing.T, operation *runtime.ClientOperation, exporter *tracetest.InMemoryExporter, expectedSpanCount int) {
110179
header := map[string][]string{}
111180
tr := newOpenTelemetryTransport(&mockRuntime{runtime.TestClientRequest{Headers: header}}, "remote_host", nil)

‎client/runtime.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,22 @@ func (r *Runtime) CreateHttpRequest(operation *runtime.ClientOperation) (req *ht
188188
return
189189
}
190190

191+
// ContextualTransport extends [runtime.ClientTransport] with an
192+
// explicit context-aware submission method.
193+
//
194+
// [Runtime] satisfies it via [Runtime.SubmitContext]. Wrappers such
195+
// as the OpenTelemetry transport type-assert to this interface so
196+
// they can forward an explicit context to the underlying transport
197+
// without setting the cached [runtime.ClientOperation.Context] field.
198+
//
199+
// In v2, SubmitContext will be folded into [runtime.ClientTransport]
200+
// itself and the cached context field removed; this interface is the
201+
// v0.x bridge.
202+
type ContextualTransport interface {
203+
runtime.ClientTransport
204+
SubmitContext(context.Context, *runtime.ClientOperation) (any, error)
205+
}
206+
191207
// Submit a request and when there is a body on success it will turn that into the result
192208
// all other things are turned into an api error for swagger which retains the status code.
193209
//

‎form.go‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"mime/multipart"
1010
"net/http"
11+
"strings"
1112

1213
"github.com/go-openapi/errors"
1314
)
@@ -281,8 +282,55 @@ func countFileParts(r *http.Request) int {
281282
return n
282283
}
283284

285+
// FormFile resolves a file field from a parsed form body, transparently
286+
// handling both content types accepted for `type: file` parameters by
287+
// the OpenAPI 2.0 spec:
288+
//
289+
// - multipart/form-data — delegates to [http.Request.FormFile].
290+
// - application/x-www-form-urlencoded — looks up the field in
291+
// r.PostForm and synthesizes a [multipart.File] backed by the
292+
// value bytes plus a [multipart.FileHeader] with Filename equal
293+
// to the field name and Size set to the byte length.
294+
//
295+
// Returns [http.ErrMissingFile] when the field is absent under either
296+
// content type. Callers must have parsed the body upstream (e.g. via
297+
// [BindForm] or [http.Request.ParseForm]) before reading from the
298+
// urlencoded path — [http.Request.FormFile] takes care of parsing on
299+
// the multipart path.
300+
//
301+
// Presence is the only criterion for binding a urlencoded file: an
302+
// empty value (e.g. `file=`) is bound as a zero-byte file.
303+
func FormFile(r *http.Request, name string) (multipart.File, *multipart.FileHeader, error) {
304+
file, header, err := r.FormFile(name)
305+
if err == nil {
306+
return file, header, nil
307+
}
308+
if !stderrors.Is(err, http.ErrNotMultipart) {
309+
return nil, nil, err
310+
}
311+
312+
values, present := r.PostForm[name]
313+
if !present {
314+
return nil, nil, http.ErrMissingFile
315+
}
316+
value := values[0]
317+
return urlencodedFile{Reader: strings.NewReader(value)},
318+
&multipart.FileHeader{Filename: name, Size: int64(len(value))},
319+
nil
320+
}
321+
322+
// urlencodedFile adapts a urlencoded form value (already buffered in
323+
// memory by [http.Request.ParseForm]) to the [multipart.File]
324+
// interface. The embedded [strings.Reader] supplies Read/ReadAt/Seek;
325+
// Close is a no-op since there is no resource to release.
326+
type urlencodedFile struct {
327+
*strings.Reader
328+
}
329+
330+
func (urlencodedFile) Close() error { return nil }
331+
284332
func bindFormFile(r *http.Request, spec formFileSpec, maxFilenameLen int) error {
285-
file, header, err := r.FormFile(spec.name)
333+
file, header, err := FormFile(r, spec.name)
286334
if err != nil {
287335
if stderrors.Is(err, http.ErrMissingFile) {
288336
if spec.required {

0 commit comments

Comments
 (0)