You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Five URL-handling issues in pre-existing camptocamp/ogc-client upstream code, surfaced during the Phase 8 root-cause investigation of the live demo's "Library strict-mode validation skipped" warning. None are in our CSAPI contribution diff — verified with git diff upstream/main.
Each finding is classified below against an evidence ladder (platform spec → MDN → library contract → library tests) so reviewers can audit the reasoning rather than take the classification on faith.
Scope and severity (read this first)
Finding Phase 1.1: Create Type System (csapi/model.ts) #1 actively breaks integrators that pass a relative baseUrl (e.g. same-origin proxy deployments such as ours). Most upstream users pass absolute URLs and do not hit this.
Finding Phase 2.1: Systems Methods #5 is low-confidence and may be working-as-designed. Included for completeness; downgrade or close at maintainer discretion.
This issue is not a claim that the library is broadly broken. It is a claim that (a) one specific deployment pattern is unsupported, (b) the implicit "absolute URL only" contract is undocumented, and (c) several call sites silently rely on that contract in ways that will eventually surface.
Plain-language framing
To state the situation as plainly as possible, so reviewers can decide whether this is worth their time:
The library's implicit input contract appears to be "absolute URL only" for the baseUrl passed to OgcApiEndpoint. That contract is undocumented in the README, type signatures, or JSDoc, but it is consistent with every example shown upstream and with the de-facto contract enforced by the test suite (relative input is never exercised — see L4).
Most users pass absolute URLs, so the library works correctly for them. We are not claiming otherwise.
Our deployment uses a same-origin reverse proxy and passes /api/csapi-go as the baseUrl. This is a legitimate, real-world pattern (CORS avoidance, cookie-domain alignment, dev/prod URL portability) but it is not the typical upstream usage. It is an edge case the library has not committed to supporting.
That edge case does break today — concretely, finding Phase 1.1: Create Type System (csapi/model.ts) #1 throws a TypeError four call levels deep — which is why our explorer ships a thin bridge component to construct an absolute URL before handing off to OgcApiEndpoint. The bridge is a workaround for our deployment pattern, not a workaround for a universal library defect.
Findings Phase 1.2: Create Helper Utilities (csapi/helpers.ts) #2–Phase 1.4: Integrate with OgcApiEndpoint #4 are separate from our blocker. They are spec-level issues (OGC API Common permits relative href per RFC 8288; the library does not resolve those) that have not surfaced for current users only because most servers in the wild emit absolute hrefs. They are worth reporting because they will surface eventually for someone, but they are not what is breaking us today.
The ask is therefore narrow: document the absolute-URL contract, or relax it for finding #1, so deployments behind same-origin proxies do not need a bridge. Findings #2–#4 are flagged for the maintainers' awareness and prioritization at their discretion.
"new URL("/en-US/docs"); // Raises a TypeError exception as '/en-US/docs' is not a valid URL"
L3 (link semantics) — OGC API – Common Part 1 §7.4 + RFC 8288 §3.1 — link href values may be relative URI references; clients are expected to resolve against a base.
L4 (library tested contract) — src/shared/url-utils.spec.ts — getBaseUrl is tested with no-arg and absolute input only; the relative-input case is not covered.
new URL(relative) throwing is correct platform behavior, not a bug. The defects below are about how upstream code uses that primitive, not the primitive itself.
The five sites (all upstream-owned)
#
Site (phase-8 line)
Classification
Why it's classified that way
1
src/shared/url-utils.ts:9 — getBaseUrl(url) → new URL(url)
Internal contract inconsistency (active for us).
getParentPath and getChildPath in the same file accept relative input and resolve via new URL(url, getBaseUrl()). They imply the helper layer supports relative input. But getBaseUrl(url) itself does not. The internal contradiction is the defect; the new URL throw is just how it manifests. L4 silent (not tested), L3 silent (not documented either way). Hits us because we pass /api/csapi-go.
2
src/ogc-api/info.ts:161 — parseBaseCollectionInfo, new URL(link.href) no base
Latent spec-vs-code defect.
OGC API Common (L3) explicitly permits relative href per RFC 8288. WHATWG URL (L1) requires a base for relative input. The library claims to consume OGC API responses, so a server returning a spec-permitted relative href will throw here. Not currently breaking us because csapi-go returns absolute hrefs.
3
src/ogc-api/edr/url_builder.ts:103,163,223,300,371,446,514 — 7× new URL(href) no base
Same as #2. OGC API – EDR (OGC 19-086) inherits Common's link semantics. Latent until an EDR server returns relative hrefs.
4
src/ogc-api/endpoint.ts:675,678,680 — new URL(href, this.baseUrl || '')
Real defect, internally inconsistent. Empty-string base is a parsing failure per WHATWG URL §4.4 ("missing-scheme-non-relative-URL validation error, return failure"). The || '' fallback is only reached when this.baseUrl is falsy — i.e., precisely when a usable base is most needed. Latent (requires a falsy baseUrl plus a relative href).
5
src/ogc-api/link-utils.ts:75 — fetchCollectionRoot, new URL(parentUrl) no base
Low confidence — possibly working as designed. All current call sites pass an already-absolute URL. No spec/test/internal contradiction. Listed for completeness; close at maintainer discretion.
Note: JavaScript evaluates the second argument of new URL(href, base) before invoking the constructor, so even though link.href is absolute, getBaseUrl(baseUrl) throws first.
// src/shared/url-utils.tsexportfunctiongetBaseUrl(url?: string): string|URL{if(url&&typeofurl==='string'){// Resolve relative paths against globalThis.location when in a browser-like// environment, matching the implicit contract already honored by// getParentPath/getChildPath in this same file.if('location'inglobalThis&&typeofglobalThis.location==='object'){returnnewURL(url,globalThis.location.toString());}returnnewURL(url);// Node: caller must still pass absolute}if('location'inglobalThis&&typeofglobalThis.location==='object'){returnglobalThis.location.toString();}returnnewURL('http://localhost');}
Plus a regression test in src/shared/url-utils.spec.ts covering relative-path input (currently absent — see L4 above).
Findings #2–#4 would each be addressed by passing an explicit base (likely the endpoint's own resolved root URL) at every new URL(href, …) call site.
Scope of this issue vs. our app's integration code
To prevent a misreading: our explorer ships an app-layer module (demo/src/csapi-bridge.ts, 614 lines) that sits between Vue components and OgcApiEndpoint/csapi(). The existence of that module is not evidence of upstream defects. It is standard application architecture — the same kind of integration layer most apps build over a generic library.
What the bridge actually contains, by category:
Discovery policy (3-tier):initializeBuilder runs strict (endpoint.csapi()) → permissive (link scan) → fallback (all 24 CSAPI types). This is opinionated UX policy ("never leave the user with an unusable UI"), not something the library should impose on every consumer.
Proxy-relative URL builders (10 of them):getListUrl, getDetailUrl, getCreateUrl, getUpdateUrl, getDeleteUrl, getNestedListUrl, getSchemaUrl, getCommandStatusUrl, getControlStreamSchemaUrl. These produce paths relative to a proxy mount that apiFetch() later prepends. The library correctly produces absolute URLs; the bridge is the seam where they get rewritten for our deployment.
CSAPI Part 2 payload helpers:parsePart2Resource, tryParseCommandStatus, getContentType, classifyResource. App-level format glue.
Failure-kind UX classification:strictModeFailureKind — pure presentation logic, added in explorer commit 72535f6 so the user sees an accurate message instead of "server conformance issue."
Only the failure-kind classifier is causally tied to the findings in this issue. It would become unnecessary if finding #1 is fixed upstream. Everything else in the bridge would still exist regardless of how this issue is resolved, regardless of whether PR #136 merges, and regardless of whether the upstream library is ever modified at all.
In other words: fixing finding #1 saves callers in our deployment pattern roughly one line of code (new URL(path, location.origin).href). It does not eliminate, and is not intended to eliminate, the need for app-side integration code.
Why we are not opening this on camptocamp/ogc-client directly (yet)
Escalation should originate from a maintainer/sponsor account once PR #136 has merged, with a full reproducer and the citations above. Filing prematurely from the PR context risks confusing the upstream review of #136.
✅ Demo-side mitigation in place: explorer commit 72535f6 reclassifies the misleading "server conformance" warning as a known library limitation linking to this issue.
Summary
Five URL-handling issues in pre-existing
camptocamp/ogc-clientupstream code, surfaced during the Phase 8 root-cause investigation of the live demo's "Library strict-mode validation skipped" warning. None are in our CSAPI contribution diff — verified withgit diff upstream/main.Each finding is classified below against an evidence ladder (platform spec → MDN → library contract → library tests) so reviewers can audit the reasoning rather than take the classification on faith.
Plain-language framing
To state the situation as plainly as possible, so reviewers can decide whether this is worth their time:
baseUrlpassed toOgcApiEndpoint. That contract is undocumented in the README, type signatures, or JSDoc, but it is consistent with every example shown upstream and with the de-facto contract enforced by the test suite (relative input is never exercised — see L4)./api/csapi-goas thebaseUrl. This is a legitimate, real-world pattern (CORS avoidance, cookie-domain alignment, dev/prod URL portability) but it is not the typical upstream usage. It is an edge case the library has not committed to supporting.TypeErrorfour call levels deep — which is why our explorer ships a thin bridge component to construct an absolute URL before handing off toOgcApiEndpoint. The bridge is a workaround for our deployment pattern, not a workaround for a universal library defect.hrefper RFC 8288; the library does not resolve those) that have not surfaced for current users only because most servers in the wild emit absolute hrefs. They are worth reporting because they will surface eventually for someone, but they are not what is breaking us today.The ask is therefore narrow: document the absolute-URL contract, or relax it for finding #1, so deployments behind same-origin proxies do not need a bridge. Findings #2–#4 are flagged for the maintainers' awareness and prioritization at their discretion.
Per governance (AI_OPERATIONAL_CONSTRAINTS.md) and the precedent of
docs/code-review/upstream-findings-report.md, we will not fix any of these inside PR #136.Full investigation report
docs/implementation/phase-8-strict-mode-validation-warning-root-cause.md(commita794387onphase-8).Symptom
Yellow card on
https://ogc-csapi-explorer.pages.devagainst the CSAPI-Go preset (which uses a same-origin/api/csapi-goproxy):The error is a
TypeErrorthrown four call levels deep inside upstream URL handling, not a server response.Authoritative sources cited
URL()constructor — https://developer.mozilla.org/en-US/docs/Web/API/URL/URLhrefvalues may be relative URI references; clients are expected to resolve against a base.src/shared/url-utils.spec.ts—getBaseUrlis tested with no-arg and absolute input only; the relative-input case is not covered.new URL(relative)throwing is correct platform behavior, not a bug. The defects below are about how upstream code uses that primitive, not the primitive itself.The five sites (all upstream-owned)
src/shared/url-utils.ts:9—getBaseUrl(url) → new URL(url)getParentPathandgetChildPathin the same file accept relative input and resolve vianew URL(url, getBaseUrl()). They imply the helper layer supports relative input. ButgetBaseUrl(url)itself does not. The internal contradiction is the defect; thenew URLthrow is just how it manifests. L4 silent (not tested), L3 silent (not documented either way). Hits us because we pass/api/csapi-go.src/ogc-api/info.ts:161—parseBaseCollectionInfo,new URL(link.href)no basehrefper RFC 8288. WHATWG URL (L1) requires a base for relative input. The library claims to consume OGC API responses, so a server returning a spec-permitted relative href will throw here. Not currently breaking us because csapi-go returns absolute hrefs.src/ogc-api/edr/url_builder.ts:103,163,223,300,371,446,514— 7×new URL(href)no basesrc/ogc-api/endpoint.ts:675,678,680—new URL(href, this.baseUrl || '')|| ''fallback is only reached whenthis.baseUrlis falsy — i.e., precisely when a usable base is most needed. Latent (requires a falsybaseUrlplus a relativehref).src/ogc-api/link-utils.ts:75—fetchCollectionRoot,new URL(parentUrl)no baseOwnership verification
For
info.tsandendpoint.ts, the defect lines fall outside our diff hunks — see report §5.2 for the hunk-by-hunk proof.Failure chain for finding #1 (the one breaking us today)
Note: JavaScript evaluates the second argument of
new URL(href, base)before invoking the constructor, so even thoughlink.hrefis absolute,getBaseUrl(baseUrl)throws first.Suggested upstream patch (resolves finding #1 only)
Plus a regression test in
src/shared/url-utils.spec.tscovering relative-path input (currently absent — see L4 above).Findings #2–#4 would each be addressed by passing an explicit base (likely the endpoint's own resolved root URL) at every
new URL(href, …)call site.Scope of this issue vs. our app's integration code
To prevent a misreading: our explorer ships an app-layer module (
demo/src/csapi-bridge.ts, 614 lines) that sits between Vue components andOgcApiEndpoint/csapi(). The existence of that module is not evidence of upstream defects. It is standard application architecture — the same kind of integration layer most apps build over a generic library.What the bridge actually contains, by category:
initializeBuilderruns strict (endpoint.csapi()) → permissive (link scan) → fallback (all 24 CSAPI types). This is opinionated UX policy ("never leave the user with an unusable UI"), not something the library should impose on every consumer.getListUrl,getDetailUrl,getCreateUrl,getUpdateUrl,getDeleteUrl,getNestedListUrl,getSchemaUrl,getCommandStatusUrl,getControlStreamSchemaUrl. These produce paths relative to a proxy mount thatapiFetch()later prepends. The library correctly produces absolute URLs; the bridge is the seam where they get rewritten for our deployment.parsePart2Resource,tryParseCommandStatus,getContentType,classifyResource. App-level format glue.builder = shallowRef<CSAPIQueryBuilder>(null). Framework binding.strictModeFailureKind— pure presentation logic, added in explorer commit72535f6so the user sees an accurate message instead of "server conformance issue."Only the failure-kind classifier is causally tied to the findings in this issue. It would become unnecessary if finding #1 is fixed upstream. Everything else in the bridge would still exist regardless of how this issue is resolved, regardless of whether PR #136 merges, and regardless of whether the upstream library is ever modified at all.
In other words: fixing finding #1 saves callers in our deployment pattern roughly one line of code (
new URL(path, location.origin).href). It does not eliminate, and is not intended to eliminate, the need for app-side integration code.Why we are not opening this on
camptocamp/ogc-clientdirectly (yet)Escalation should originate from a maintainer/sponsor account once PR #136 has merged, with a full reproducer and the citations above. Filing prematurely from the PR context risks confusing the upstream review of #136.
Disposition
SystemTypeUrisname collision between model.ts and constants.ts #136. Governance forbids it.SystemTypeUrisname collision between model.ts and constants.ts #136's context.SystemTypeUrisname collision between model.ts and constants.ts #136 merges.72535f6reclassifies the misleading "server conformance" warning as a known library limitation linking to this issue.Related work
checkHasConnectedSystemsuses draft-era conformance URIs not declared by spec-conformant servers (csapi-go interop blocker) #186 —checkHasConnectedSystemsPart 1 / Part 2 prefix support — OURS, fixed inc323e06.maincommit72535f6.Labels
upstream,deferred,do-not-fix-in-pr-136,phase-8