Fix double-slash URL path bug across 74 GetRequest methods - #804
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #804 +/- ##
===========================================
- Coverage 73.45% 52.51% -20.95%
===========================================
Files 431 990 +559
Lines 16398 59059 +42661
===========================================
+ Hits 12045 31014 +18969
- Misses 2705 24939 +22234
- Partials 1648 3106 +1458
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
Thanks for the fix! Have you had a chance to verify that this same issue doesn't exist in other APIs as well? |
|
Yes, it's fixed now for every call site, not just the |
Jakob3xD
left a comment
There was a problem hiding this comment.
I mislike that concept of omitting required values that are empty. Doing so can cause a lot of different behaviours. The correct approach would be to validate the user input, to not run into the double slash situation.
|
Heh, I agree — and almost went with something closer to my conventional approach, which is to define a type for each URL segment and pass a config struct with proper validation. Let me update this PR to match the revised approach, since I'm on the same page but didn't want to overdo it given the ubiquity and surface area of the change (even if it's mechanical). Will push a follow-up shortly. |
|
@Jakob3xD What do you think of this? New (proposed): Where For call sites where the fields are known to be populated, |
0b8c011 to
e88cf98
Compare
|
Alright, this has been pushed up. It was a large but very mechanical change. Every The change intentionally keeps all published API types ( I think this is a net positive even though the diff is sweeping: the boilerplate to define the path structs eliminates an entire class of bugs (e.g. empty segment -> double slash -> authority confusion) and by making the path shape explicit and testable. For the future |
Replace manual strings.Builder URL path construction with typed path builder structs that reject empty required segments. Fixes an issue where empty path segments produced a double-slash // that http.NewRequest misparsed as an RFC 3986 authority separator. Ref: opensearch-project#804 Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Replace manual strings.Builder URL path construction with typed path builder structs that reject empty required segments. Fixes an issue where empty path segments produced a double-slash // that http.NewRequest misparsed as an RFC 3986 authority separator. Ref: opensearch-project#804 Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Replace manual strings.Builder URL path construction with typed path builder structs that reject empty required segments. Fixes an issue where empty path segments produced a double-slash // that http.NewRequest misparsed as an RFC 3986 authority separator. Ref: opensearch-project#804 Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Replace manual strings.Builder URL path construction with typed path builder structs that reject empty required segments. Fixes an issue where empty path segments produced a double-slash // that http.NewRequest misparsed as an RFC 3986 authority separator. Ref: opensearch-project#804 Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Replace manual strings.Builder URL path construction with typed path builder structs that reject empty required segments. Fixes an issue where empty path segments produced a double-slash // that http.NewRequest misparsed as an RFC 3986 authority separator. Ref: opensearch-project#804 Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
d1bb640 to
af1c4eb
Compare
|
@Jakob3xD Can you give this another shake or bless this? |
de9fdcc to
43f71d4
Compare
Replace all fmt.Sprintf URL path construction with typed ospath builders (IndicesCreatePath, IndicesDeletePath, SearchPath, SearchShardsPath, IndexPath, ClusterHealthPath, ClusterStatePath) for compile-time safety and proper percent-encoding of path segments. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Each per-node FSM packs an ordinal Layer (lower 16 bits, cumulative) and a free-form ClientState (upper 16 bits) into one atomic uint32 so the satisfaction check collapses to s & target == target. Layers gate each other (no LayerHTTP without LayerTCP); ClientState bits flap freely. Per-layer budgets are tuned for CI pessimism. NodeFSM.Advance captures the caller's intent at the first state observation: a forward call (next > initialLayer) that loses the CAS to a peer that already advanced past next is a silent no-op, not a regression. Without this, two concurrent forward Advance calls would mis-classify the loser's retry against a now-higher layer as a deliberate regression and trip MaxRegressions. Cluster.lastPolledAt sits inside c.mu alongside lastErr/lastResp/polls with SetLastPolledAt/LastPolledAt accessors so the diagnostic dump reads a consistent snapshot. NodeFSM.Snapshot returns state, enteredAt, regressions, and history under the FSM's mu so renderNode emits a coherent per-node line. The old four independent loads could skew across a concurrent transition mid-render. The misconfig guard fails fast on any non-zero target without a LayerCheck or FSMCheck (was: only layer-bearing targets), so a state-only target with no driver no longer silently times out. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
The previous implementation polled cat-nodes for 60s with a single boolean predicate, producing "Condition never satisfied" on timeout. Switch to readiness.Wait(t, ctx, TargetClusterReady, WithCluster(c)) which observes per-node progression through LayerHTTP, LayerClusterJoin, and LayerStatsReady, and dumps a structured per-node diagnostic with the full last cat-nodes response on failure. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
…window /_cat/shards parsing skips RELOCATING and INITIALIZING rows, so a shard mid-relocation is absent from placement.ShardToNodes for the duration of the relocation window. The previous unconditional store would clobber a complete cached map with this partial response, leaving Shards[N] == nil for the in-flight shard and disabling shard-exact routing for it. Carry forward entries the new response is missing: a shard in RELOCATING state still serves reads from the source until the destination flips to STARTED, so the prior entry remains a valid routing target for the relocation window. New data wins where present. Use CompareAndSwap with a bounded retry to bound the visible Load+merge+Store window: a CAS loss can drop a strictly fresher in-flight observation, so cheap retries (3 attempts) are preferred over a single attempt with silent loss. Log on retry exhaustion to surface persistent contention from a hot health-check goroutine; the next merge cycle will self-heal. Compute NumberOfPrimaryShards from the cross-state primary set (counting distinct shard numbers across STARTED + RELOCATING + INITIALIZING rows) rather than from ShardToNodes alone. ShardToNodes is filtered to STARTED entries, so a relocating shard would otherwise undercount the primary count and cause the post-merge prune to drop the very entry that carry-forward just preserved. The prune now fires only when the placement covers every primary shard (a real shrink), never on a partial view. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Route all four singleServerPool construction sites through a newSingleServerPool helper that CAS's lcActive (clearing lcUnknown|lcStandby) under conn.mu; without it the no-op OnSuccess/OnFailure leave the connection lcUnknown for the lifetime of the pool, so observers classify the only available connection as not-ready. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
guides/routing.md documents the post-warmup state as lcReady|lcActive but tryWarmupSkip's completion path only cleared lcNeedsWarmup, leaving lcReady unset for the lifetime of the connection. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
config v1.32.17 -> v1.32.18 credentials v1.19.16 -> v1.19.17 ssooidc v1.35.21 -> v1.36.0 Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Add a package-local unexported ptr() helper to the opensearch and opensearchapi packages and route ToPointer through it. ToPointer is flagged Deprecated; the helper is removed in v5. Once the module's go directive moves to 1.26, ptr() itself can be deleted and call sites can switch to the native new(value) form (e.g. new(false)). Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
- Target rendering uses runtime.NumCPU() goroutines with sorted output for determinism. - Added sync.WaitGroup.Go() workers in api_cmd renderloop - Split Makefile gen into gen-paths and gen-api targets for make -j gen support Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
- All 352 boolean query parameters now emit as *bool, enabling callers
to send both true and false values. Fixes inability to override
server-default-true params (wait_for_completion, flush, etc).
- classifyParamSchema returns "*bool" instead of "bool" for booleans
- Encoder: if r.X != nil { set("name", strconv.FormatBool(*r.X)) }
- All test expectations updated
Fixes: opensearch-project#840
Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Add a `cmd/osgen/` exclusion to .golangci.yml's goconst rule (the generator uses many short repeated literal strings as identifiers and import paths inside template input that don't benefit from const extraction). Extend the `lint.local` Makefile target to run golangci-lint inside cmd/osgen too, since it's a separate module not covered by the root invocation. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
- F18: emit opExplainCheck for case=1 + positional deps, closing a silent-fall-through path the explainXxx helper was meant to catch. - F24: unexport the path.Builder interface; the assertions are intra-package and no external caller writes path.Builder. - F25: drop the unused paramSet accumulator in pathParamInfo. - F26: iterate spec.Paths and per-path Operations() in sorted order so description, versionAdded, and docsURL captured for multi-method URLs are deterministic instead of depending on map iteration order. - F27: panic on unknown HTTP methods in httpMethodConst instead of silently falling through to MethodGet, so a spec typo or new verb fails the generator loudly. - F36: clarify alwaysImplies vacuous-case behavior; the function intentionally returns false when no path contains the dependent. Regenerated internal/path/builders_gen.go and builders_gen_test.go. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
…tests
Improve generator-emitted test coverage:
- Path test emit (cmd/osgen/paths_testgen.go): for builders with 2+
required fields, emit a "missing <Field>" case per required field
with the others set; for builders with list fields, emit a multi-value
case for each list field, not just the first; for builders with only
optional fields, emit "only <FieldName>" cases per field. Replace
require.Error with require.ErrorIs against errRequired so the public
error contract is verified, not just presence.
- Params test emit (cmd/osgen/emit/build.go, frag_tests.go): emit two
scenarios for each *bool query param ("=true" and "=false") so the
wire-level encoding of an explicit false is exercised.
Regenerate internal/path/builders_gen_test.go and the
osapi/*_internal_gen_test.go family.
Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Replace the per-segment self-consistency round-trip with a stricter helper that catches structural bugs the encoder is meant to prevent: - url.ParseRequestURI must succeed (valid request-URI shape). - Path must not contain "//" (the original opensearch-project#650 double-slash bug). - Decoded segments must not still decode further (no double-encoding). - Decoded segments must not contain "/", "?", "#" (path injection). - Stable encode-decode round-trip on every sub-segment. Used by every generated builder test. Catches encoder regressions that the previous self-consistency check would have silently accepted. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
- Run golangci-lint --fix across cmd/osgen (a separate Go module).
Mostly comment additions and minor formatting; no behavior change.
- Remove the dead pre-emit/ render pipeline:
cmd/osgen/api_render.go (843 lines)
cmd/osgen/api_render_plugin_client.go (207 lines)
cmd/osgen/api_testgen.go (448 lines)
cmd/osgen/api_testgen_integ.go (421 lines)
cmd/osgen/render_integration_test.go (596 lines, the only consumer)
These were superseded by cmd/osgen/emit/. The active code path runs
through emit.Build; the deleted files were called only from the
legacy integration test that goes with them. resolveFieldPath, the
only function that survived as a live dependency from this set, is
preserved next to subClientHierarchy in cmd/osgen/route.go.
Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
The dispatch template at cmd/osgen/emit/frag_dispatch.go used
`{{- ""}}` followed by a `{{- if ...}}` to strip whitespace, which
also stripped the space between "req" and the type name. The
generator emitted "func (c Client) Bulk(ctx context.Context, reqBulkReq)"
- a syntactically invalid parameter list - whenever IsPointerReq was
false (value-receiver Req struct).
Replace the multi-line whitespace-stripping construct with a single
inline `{{if $.IsPointerReq}}*{{end}}` so the template emits "req X"
for value Req and "req *X" for pointer Req correctly.
The bug only surfaced when generating the value-receiver dispatch path
(Bulk, Create, Delete, etc.); pointer-receiver Req types worked because
the leading `*` masked the missing space.
Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Relocate the generated v5-track API surface from `osapi/` to `v5preview/opensearchapi/` ahead of merging this PR. Two motivations: - Signal preview status: the `v5preview/` parent directory makes the package's stability guarantee explicit at the import path. Callers importing `.../v5preview/opensearchapi` cannot mistake it for the stable v5 surface. - Reserve the canonical name: the package is already named `opensearchapi` (matching the legacy hand-written package). Once v5 is cut and the legacy package is removed, the v5preview/ prefix gets dropped and consumers move with a single import-path edit; the type qualifiers (`opensearchapi.IndexReq`, etc.) stay constant. Generator changes that make the relocation a one-line config swap later: - cmd/osgen/ir/defaults.go (new): centralizes ModulePath, DefaultCorePkgName, DefaultCoreSubpath, DefaultCoreImportPath, and DefaultPluginsImportBase. Promoting v5preview/opensearchapi to the module root reduces to editing DefaultCoreSubpath. - cmd/osgen/emit/build.go: introduces coreImportPath() to centralize import-path resolution; importPathForGroup and the dispatch test emitter route through it. - Plugin test helper directory now uses `internal/<pkg>test` instead of `internal/test`, so the helper subpackage is unique per plugin (avoids collisions when multiple generated packages share the same module). Companion edits across CHANGELOG.md, UPGRADING.md, DEVELOPER_GUIDE.md, .codecov.yml, .github/workflows/check-gen.yml, .golangci.yml, Makefile, guides/security.md, and cmd/osgen/README.md update path references; the file moves themselves are pure git renames preserving history. The follow-up commit regenerates the package contents at the new location. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Pure regeneration output: `make gen` after the prior commit moved the package and updated cmd/osgen's defaults to emit v5preview/opensearchapi/. No semantic change. The 6042/6042 line balance reflects two mechanical substitutions across 1590 generated files: - `package osapi` -> `package opensearchapi` (and `osapi_test` -> `opensearchapi_test` for external test files). - Import path `github.com/opensearch-project/opensearch-go/v4/osapi` -> `.../v4/v5preview/opensearchapi`, with the qualifier `osapi.` -> `opensearchapi.` updated at every reference inside the regenerated test fixtures. No hand-edits in this commit; any review should focus on the prior move/codegen-config commit. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
The "release clears pointer references across full capacity" subtest read the backing array after b.Release(), racing with parallel rendezvousTopK calls that re-acquired the same buffer from connSlicePool. Factor the clear-and-reset logic into a shared clearConns helper and test it directly; same invariant coverage with no use-after-Release. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
|
Run 26578837681 / Job 78305677820 — failure is not caused by this PR. What hung
Why the gate never advanced
node2 joined the cluster ( |
clusterLensFSMCheck polls cat-nodes to advance LayerStatsReady. Without ?timeout=, cat-nodes' inner NodesInfo+NodesStats RPCs (server-side at RestNodesAction.java:128,140) inherit the unbounded request default. A freshly joined node whose first stats cycle stalls leaves the row with cpu=null, heap.percent=null indefinitely, and the readiness gate hangs until the per-layer budget elapses; CI run 26578837681 hit this on 2.13.0 insecure with TestNodesInfo + TestNodesStats consuming the full package timeout. 10s bounds each server-side fan-out and lets the Go-side polling cadence drive retries instead of relying on individual server-side calls to complete. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
Summary
When optional path segments (
Indices,Index,Repo,TaskID, etc.) are empty,GetRequest()methods produced a double-slash (e.g.//_alias/myalias) whichhttp.NewRequestmisparses per RFC 3986, treating the next segment as the URL authority. AftersetReqURLoverwrites the host, the path loses its API prefix entirely, causing requests to hit the wrong endpoint.Observed symptoms:
AliasPutReqwith emptyIndicescreates an index instead of an aliasAliasGetReqwhen no indices are specified #617:AliasGetReqwith emptyIndicesreturns wrong error format (hits index lookup instead of alias lookup)Root cause: 74
GetRequest()methods acrossopensearchapi,plugins/security, andplugins/ismunconditionally wrote"/" + variableinto the URL path without guarding against empty values.Fix: Extract
opensearch.BuildPathhelper that skips empty segments, preventing the double-slash class of bug. All 74 vulnerable methods converted to use it. Benchmarked at 1 alloc / ~21ns vspath.Join's 3 allocs / ~63ns.Fixes: #617, #650
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.