Skip to content

fix: resolve extension resources for the target platform VS Code asks for - #2183

Merged
netomi merged 6 commits into
mainfrom
fix/unpkg-target-platform
Sep 25, 2026
Merged

netomi merged 6 commits into
mainfrom
fix/unpkg-target-platform

Conversation

@netomi

@netomi netomi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2181 — base is fix/target-platform-validation, so this diff shows only the unpkg change. #2181 supplies the @Validated/@Pattern wiring on VSCodeAPI that this reuses; without it the two PRs would both add those lines and conflict. Merge that first, or say so and I will retarget this at main.

Reimplements #758 (June 2023) on current main. Fixes #744.

What is broken

VS Code encodes the target platform into the version segment when it resolves an extension's resources — <version>+<target>, web for a web extension (microsoft/vscode#180525, microsoft/vscode#182072). The unpkg endpoint never learned to read it:

/vscode/unpkg/BroadcomMFD/hlasm-language-support/1.23.0/extension/package.json       → 200
/vscode/unpkg/BroadcomMFD/hlasm-language-support/1.23.0+web/extension/package.json   → 404
/vscode/unpkg/.../1.23.0%2Bweb/extension/package.json                                → 404

That extension really does publish 1.23.0 for both universal and web, and the second URL is what a web VS Code sends — so its resources do not load at all in vscode.dev or github.dev.

Even without the suffix the endpoint could not have told the two apart: it passed null down to the download lookup, which matched whichever version came first.

The change

Smaller than #758, because the plumbing already takes a target platform — WebResourceService.getExtensionDownload(namespace, extension, targetPlatform, version) and browseExtensionPackage(...) both have the parameter, and browseExtensionPackage was using it only to compose an error message. So this is mostly a matter of resolving a target at the edge and passing it down.

Where the target comes from. ?target= if given, otherwise a +<target> suffix on the version. The suffix is taken only when it names a platform:

var separator = version.lastIndexOf('+');
if (separator >= 0 && separator + 1 < version.length()) {
    var candidate = version.substring(separator + 1);
    if (TargetPlatform.isValid(candidate)) { ... }
}

A version may legitimately carry semver build metadata — 1.2.3+build.5 — which SemanticVersion.VERSION_PATH_PARAM_REGEX explicitly permits. "Text after the last +" would read build.5 as a target, so the validation is what makes the heuristic safe rather than merely convenient. There is a test for it.

Directory listings re-append the target. browseExtensionPackage built its child URLs from the bare version, so without this, walking into a subdirectory silently drops back to whichever version matches first — the same bug one level down. universal is left off, since it is the absence of a target rather than a target.

Upstream forwarding. UpstreamVSCodeService passes the target on as ?target=, so a mirror forwards it rather than resolving it against its own registry. Query-parameter form rather than the + suffix because both are accepted and it needs no escaping decisions.

Tests

  • testBrowseTopDirForATargetPlatformInTheVersion — 0.16.6+web resolves, and the returned listing URLs keep +web
  • testBrowseTopDirForATargetPlatformParameter — the same via ?target=web
  • testBrowseKeepsSemverBuildMetadataInTheVersion — 1.2.3+build.5 is looked up whole, with a null target, verified on the repository call

The first two fail without the change. The third passes either way, which is right — it guards against over-eager parsing, so it should hold both before and after.

Full server suite green (1214 tests); spotlessCheck and format.sh clean.

On #758

It cannot be rebased: it is written against ResponseEntity<byte[]> and a codebase with no WebResourceService, across 8 files and 2½ years of drift. Its repository addition is also redundant now — findFileByType already resolves a target-specific version. The design is amvanbaren's and this follows it, including the two details easiest to miss: validating the suffix before stripping it, and putting the target back on the listing URLs.

Suggest closing #758 in favour of this.

@netomi
netomi force-pushed the fix/target-platform-validation branch from 956e625 to c8b9535 Compare September 9, 2026 12:27
@netomi
netomi force-pushed the fix/unpkg-target-platform branch from 8f987a5 to c3a4a4c Compare September 9, 2026 12:27
Base automatically changed from fix/target-platform-validation to main September 9, 2026 12:27
@netomi
netomi requested a lite review from Copilot September 10, 2026 19:33
@netomi
netomi force-pushed the fix/unpkg-target-platform branch from c3a4a4c to 8273ae9 Compare September 10, 2026 19:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

VSCodeAPI validates targetPlatform with a regex that allows empty strings on endpoints with a default value, which can allow ?targetPlatform= to pass validation but then incorrectly 404 due to downstream lookups using "" as the platform.

Pull request overview

This PR updates the VS Code compatibility layer so /vscode/unpkg/** resolves and browses extension resources for the target platform VS Code requests (either via ?target= or a +<target> suffix on the version), and adds request-side validation for targetPlatform where it’s supplied as a query/body parameter.

Changes:

  • Parse target platform for unpkg browsing from ?target= or a validated +<target> version suffix and pass it through to the browse services.
  • Preserve the target platform when generating directory listing URLs so subsequent navigation doesn’t “fall back” to an arbitrary matching version.
  • Enforce targetPlatform allowable values via Bean Validation annotations across registry and VS Code adapter endpoints, with tests covering rejections/acceptance.
File summaries
File Description
server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java Adds tests asserting unknown targetPlatform query/body values are rejected.
server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java Adds tests for unpkg browse behavior with +web and ?target=web, plus semver build-metadata guard.
server/src/test/java/org/eclipse/openvsx/adapter/UpstreamVSCodeServiceTest.java Updates tests for the new browse(..., targetPlatform, ...) signature.
server/src/main/java/org/eclipse/openvsx/util/TargetPlatform.java Introduces NAMES_PARAM_REGEX for validating query/body params (allowing empty/absent).
server/src/main/java/org/eclipse/openvsx/RegistryAPI.java Adds @Pattern validation to targetPlatform query params and @Valid for POST body validation.
server/src/main/java/org/eclipse/openvsx/json/QueryParamJson.java Adds @Pattern validation for request-body targetPlatform.
server/src/main/java/org/eclipse/openvsx/adapter/WebResourceService.java Re-appends target to directory listing URLs (except universal).
server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java Adds method parameter validation; resolves unpkg target from query or version suffix and passes it downstream.
server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java Forwards browse target upstream via ?target=.
server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java Threads targetPlatform through unpkg browsing to ensure correct download lookup and browsing.
server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java Extends browse contract to include targetPlatform.
Review details

Suppressed comments (2)

server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java:228

  • TargetPlatform.NAMES_PARAM_REGEX intentionally allows an empty string, but on this endpoint an empty ?targetPlatform= overrides the defaultValue and then propagates as "" into repository lookups (via LocalVSCodeService), which can lead to an incorrect 404. Since this endpoint already has a default (universal), it’s safer to reject an explicit empty value by validating against NAMES_PATH_PARAM_REGEX (non-empty) instead.

This issue also appears on line 350 of the same file.

            @RequestParam(defaultValue = TargetPlatform.NAME_UNIVERSAL)
            @Pattern(
                regexp = TargetPlatform.NAMES_PARAM_REGEX,
                message = "parameter must be a supported target platform"
            )

server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java:354

  • TargetPlatform.NAMES_PARAM_REGEX allows an empty string, but on this endpoint an empty ?targetPlatform= overrides the defaultValue and then propagates as "" into repository lookups (via LocalVSCodeService), which can lead to an incorrect 404. Since this endpoint already has a default (universal), it’s safer to reject an explicit empty value by validating against NAMES_PATH_PARAM_REGEX (non-empty) instead.
            @RequestParam(defaultValue = TargetPlatform.NAME_UNIVERSAL)
            @Pattern(
                regexp = TargetPlatform.NAMES_PARAM_REGEX,
                message = "parameter must be a supported target platform"
            )
  • Files reviewed: 7/7 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@netomi

netomi commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Re: Copilot's review — the suppressed comments on VSCodeAPI.java:228 and :354 about ?targetPlatform= (empty value) bypassing the universal default and propagating as "" into repository lookups:

This doesn't reproduce. @RequestParam(defaultValue = ...) treats a present-but-empty value the same as an absent one — that's the documented contract (defaultValue: "used as a fallback when the request parameter is not provided or has an empty value"), and it's enforced in AbstractNamedValueMethodArgumentResolver.resolveArgument, which explicitly re-substitutes the default when the resolved arg equals "". So ?targetPlatform= on getAsset/download resolves to "universal" before the @Pattern check ever runs, not "".

I confirmed this with a test (?targetPlatform= against /vscode/asset/...): it does not 404 the way an unrecognized value like win32-bogus does, and swapping NAMES_PARAM_REGEX for NAMES_PATH_PARAM_REGEX (rejecting empty) makes no behavioral difference, since the empty string never reaches validation. Leaving the code as-is.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Core upstream target forwarding lacks coverage, and the endpoint’s documented 400 response is incomplete.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)

@netomi
netomi requested a balanced review from Copilot September 25, 2026 19:01
@netomi
netomi removed this pull request from stack #2184 September 25, 2026 19:13
netomi and others added 2 commits September 25, 2026 19:14
… for

VS Code encodes the target platform into the version segment when it resolves
an extension's resources - `<version>+<target>`, `web` for a web extension
(microsoft/vscode#180525). The unpkg endpoint never learned to read it, so such
a request 404s:

    /vscode/unpkg/BroadcomMFD/hlasm-language-support/1.23.0/...       200
    /vscode/unpkg/BroadcomMFD/hlasm-language-support/1.23.0+web/...   404

That extension publishes 1.23.0 for both `universal` and `web`, and the second
URL is the one a web VS Code sends, so its resources do not load at all.

Without a target the endpoint also could not have distinguished them: it passed
null down to the download lookup, which then matched whichever version came
first. The plumbing already took a target platform - WebResourceService's
getExtensionDownload and browseExtensionPackage both have the parameter, and
browseExtensionPackage used it only to compose an error message - so this is
mostly a matter of resolving one at the edge and passing it along.

The target comes from `?target=`, or from a `+<target>` suffix on the version
when that is absent. The suffix is only taken when it names a platform: a
version may legitimately carry semver build metadata (`1.2.3+build.5`), which
SemanticVersion's own regex permits, and that belongs to the version.

Directory listings re-append the target to the version in the URLs they hand
back. Without that, walking into a subdirectory silently drops to whichever
version matches first, which is the same bug one level down.

UpstreamVSCodeService forwards the target as a query parameter, so a mirror
passes it on rather than resolving it against its own registry.

Reimplements #758, which reported and fixed this in June 2023 - credit to
@amvanbaren. That PR cannot be rebased: it is written against
ResponseEntity<byte[]> and a codebase without WebResourceService, and the
repository method it adds is now redundant since findFileByType already
resolves a target-specific version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UpstreamVSCodeServiceTest still had a call to browse() with the old
4-argument signature, which no longer compiled once targetPlatform was
added to the interface. Fix it and add a test that actually exercises
the new upstream ?target= forwarding, since the existing calls all
passed null and never covered that branch.

Also document the second 400 cause (an unsupported target platform)
on the browse endpoint's OpenAPI response, alongside the existing
built-in-namespace case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@netomi
netomi force-pushed the fix/unpkg-target-platform branch from ec0c4f9 to 721b674 Compare September 25, 2026 19:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The 237-file scope contradicts the stated unpkg-only purpose, and target suffix parsing misinterprets valid versions such as 1.2.3+web.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

netomi and others added 2 commits September 25, 2026 19:21
A published version's own semver build metadata can itself name a
platform, e.g. 0.16.6+web. The suffix-stripping added for VS Code's
<version>+<target> browse requests read that as version 0.16.6 on
target web, so a universal build actually published under 0.16.6+web
could never be reached.

Try the version as given first; only fall back to splitting off a
target suffix once that lookup comes back empty.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ral version

Checking the version exactly as given before the split reading meant
every ordinary <version>+<target> request - the case VS Code actually
sends, and the reason this suffix handling exists - paid for a doomed
lookup before the one that succeeds. Swap the order: the split reading
runs first, and the literal version is only tried if that misses, so
the common case stays a single lookup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Universal-target directory links can lose their target and subsequently resolve the wrong extension build.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (1)

Browsing a version whose own suffix looks like a target (e.g.
0.16.6+web) with an explicit ?target=universal resolved correctly, but
the returned directory listing dropped the target for universal
builds. Following one of those URLs re-enters with no target
parameter, so the split reading takes over and misreads the suffix as
the target - resolving the wrong build.

Only omit the target from listed URLs when the version's own suffix
isn't itself ambiguous; keep it otherwise, universal included. Factor
the version-suffix check into TargetPlatform.targetInVersionSuffix so
VSCodeAPI's split reading and this share one implementation.

Also fix a stale test name/comment left over from reordering the
split vs. exact-version lookup in the previous commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The new parameter needs consistent OpenAPI enumeration and validation regression coverage.

Review effort: Balanced
Findings: None

Resolved since last review (2)
Previously missed (2)

In code that hasn't changed since last review

Low severity Add invalid target-platform validation test for /vscode/unpkg

server/​src/​main/​java/​org/​eclipse/​openvsx/​adapter/​VSCodeAPI.java:440

The newly added validation path is not covered. VSCodeAPITest verifies invalid target-platform rejection for the existing asset and vspackage parameters, but only valid target values are exercised for /vscode/unpkg. Add a request with target=win32-bogus that asserts the 400 response so the endpoint's dependency on class-level @Validated and this parameter constraint cannot regress unnoticed.

Low severity Document allowable target-platform values in OpenAPI schema

server/​src/​main/​java/​org/​eclipse/​openvsx/​adapter/​VSCodeAPI.java:446

The new public target parameter omits the allowableValues schema used by the other target-platform parameters in this class (for example, VSCodeAPI.java:229-248). Consequently, the generated OpenAPI contract does not expose the finite set this endpoint accepts even though validation rejects every other value. Add the same platform schema here.

…tion

The browse endpoint's new target parameter validates against the same
set of platforms as targetPlatform elsewhere in this class, but was
missing the matching allowableValues schema, so the generated OpenAPI
contract didn't expose it. Add it, and add a regression test for the
existing 400 rejection, mirroring the coverage the other
target-platform parameters already have.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@netomi

netomi commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the two "previously missed" items from the latest Copilot review, fixed in f6d9703:

  • Added the allowableValues schema to the target parameter on browse, matching the targetPlatform parameter elsewhere in this class.
  • Added testBrowseRejectsAnUnknownTargetPlatform, covering the existing 400 rejection for /vscode/unpkg the way testAssetRejectsAnUnknownTargetPlatform and testVspackageRejectsAnUnknownTargetPlatform already cover the other endpoints.

@netomi
netomi merged commit a6cfaf7 into main Sep 25, 2026
4 of 5 checks passed
@netomi
netomi deleted the fix/unpkg-target-platform branch September 25, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adopt to new web platform specifier extension resource url format

2 participants