Skip to content

fix: restore response compression for VS Code Gallery API JSON responses - #2073

Merged
netomi merged 5 commits into
mainfrom
fix/compression-accept-header-params
Aug 18, 2026
Merged

netomi merged 5 commits into
mainfrom
fix/compression-accept-header-params

Conversation

@netomi

@netomi netomi commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What

Fixes #2071: response compression silently stopped applying to VS Code Gallery API JSON responses (e.g. /vscode/gallery/extensionquery) after the Spring Boot 4 migration.

Root cause

  • Spring's content negotiation treats a requested media type with more parameters as "more specific" than the plain type declared via produces, so a client sending Accept: application/json;api-version=3.0-preview.1 got that parameter echoed back verbatim as the response Content-Type. Unchanged behavior between Spring Framework 6.2 and 7 - verified by decompiling AbstractMessageConverterMethodProcessor/MimeType from both versions.
  • The regression is that Boot 4 bumped Jetty from 12.0.x to 12.1.x, which replaced GzipHandler with a new CompressionHandler/CompressionConfig API. The old GzipHandler stripped all Content-Type parameters before matching against server.compression.mime-types; the new CompressionHandler only strips charset (MimeTypes.getContentTypeWithoutCharset), then does an exact string match. Any other parameter left in place - like an echoed api-version - breaks the match and compression is silently skipped.
  • It turns out the real VS Code Marketplace does include api-version in its Gallery API response Content-Type, so simply stripping the parameter (my first attempt) would have broken client compatibility.

Fix

  • VSCodeGalleryContentTypeAdvice - a ResponseBodyAdvice scoped to VSCodeAPI that always stamps the server's own fixed IVSCodeService.GALLERY_API_VERSION (3.0-preview.1, promoted out of VSCodeIdService where it was private) onto Gallery JSON responses, regardless of what the client's Accept header asked for (or omitted). This matches the real Marketplace's behavior and turns the Content-Type back into a single known literal instead of a client-controlled value.
  • VSCodeGalleryCompressionConfig - rather than requiring every deployment to add that literal to server.compression.mime-types, this is a WebServerFactoryCustomizer<ConfigurableJettyWebServerFactory> that reads back the CompressionConfig spring-boot-jetty already installed from server.compression.* and replaces it with an equivalent one that additionally includes the Gallery API's fixed Content-Type. Compression works regardless of configuration; a no-op if compression is disabled. Guarded by @ConditionalOnClass so a hypothetical non-Jetty deployment isn't affected.

Testing

  • VSCodeGalleryContentTypeAdviceTest / VSCodeAPITest - unit + MockMvc coverage that the fixed api-version is stamped regardless of the client's Accept header.
  • VSCodeGalleryCompressionTest - boots a real embedded Jetty server (MockMvc never touches the actual compression handler chain) and proves: (1) the bare application/json entry alone no longer compresses the versioned Content-Type (reproducing the bug against real Jetty), (2) adding the literal restores compression, and (3) the VSCodeGalleryCompressionConfig customizer alone restores compression with the literal still absent from configured mime types - the body is gunzip-decoded and checked against the original.
  • VSCodeGalleryCompressionConfigTest - ApplicationContextRunner + FilteredClassLoader proving the @ConditionalOnClass guard actually skips the configuration when Jetty's compression support isn't on the classpath, and registers it when it is.
  • Ran the full IntegrationTest (real app context, real embedded Jetty, Postgres via Testcontainers) to confirm the new bean wires in cleanly.

🤖 Generated with Claude Code

netomi and others added 4 commits August 18, 2026 10:13
…ression working

Real VS Code clients rely on the Gallery API's response Content-Type
carrying an api-version media type parameter, e.g.
application/json;api-version=3.0-preview.1, matching the real VS Code
Marketplace. Spring's content negotiation already gets most of the way
there on its own: a requested media type with more parameters is
considered "more specific" than the plain type declared via produces,
so it echoes the client's Accept-header parameter back verbatim.

Relying on that echo is fragile in two ways: it depends entirely on
whatever the client happened to send (or omitted), and it makes the
response Content-Type client-controlled, which breaks compression.
Jetty 12.1's new CompressionHandler (pulled in by the Spring Boot 4
migration) matches Content-Type against server.compression.mime-types
with an exact string comparison after stripping only the charset
parameter - any other parameter left in place, like an arbitrary
echoed api-version, fails that match and silently disables compression
for these responses. There is no way to enumerate every value a client
might send, so compression could never reliably apply to them.

Add VSCodeGalleryContentTypeAdvice, a ResponseBodyAdvice scoped to
VSCodeAPI that always stamps the server's own, fixed
IVSCodeService.GALLERY_API_VERSION onto JSON Gallery responses,
regardless of what the client asked for. That keeps the Content-Type a
single known literal - application/json;api-version=3.0-preview.1 -
which needs to be added to server.compression.mime-types alongside
application/json for compression to apply to these responses.

Also promotes the previously VSCodeIdService-private API_VERSION
constant to IVSCodeService.GALLERY_API_VERSION, shared between the
outgoing upstream requests and this server's own Gallery responses.

Fixes #2071

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MockMvc-based tests never touch the actual Jetty handler chain that
performs response compression, so they can assert the Content-Type
header is correct but can't prove compression itself still works.

Add VSCodeGalleryCompressionTest, which boots a real embedded Jetty
server via JettyServletWebServerFactory - the same class Spring Boot 4
wires up server.compression through - and shows:

- with only "application/json" configured (the Spring Boot 3 / Jetty
  12.0 setup), a response whose Content-Type is
  "application/json;api-version=3.0-preview.1" (what
  VSCodeGalleryContentTypeAdvice now stamps) is NOT compressed;
- adding that exact literal to the configured mime types restores
  compression, and the resulting body is genuinely valid gzip that
  decodes back to the original JSON.

Refs #2071

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

VSCodeGalleryContentTypeAdvice needs application/json;api-version=3.0-preview.1
added to server.compression.mime-types for Jetty 12.1's CompressionHandler
to compress those responses (exact string match, only charset stripped).
That requires every deployment's config to know about a VS Code Gallery
API implementation detail.

Add VSCodeGalleryCompressionConfig instead: a
WebServerFactoryCustomizer<ConfigurableJettyWebServerFactory> that, once
spring-boot-jetty has installed its CompressionHandler from whatever
server.compression.* is configured, reads back that installed
CompressionConfig and replaces it with an equivalent one that
additionally includes the Gallery API's fixed Content-Type - so
compression works for it regardless of server.compression.mime-types.
If compression is disabled, no CompressionHandler is installed and this
is a no-op.

This trades a config-only fix for a code-only one: it adds a
compile-time dependency on jetty-compression-server (otherwise only a
transitive runtime dependency of spring-boot-starter-jetty) and reaches
into Jetty-specific classes. Verified end-to-end against a real
embedded Jetty server, and against the full application context booted
on Jetty (IntegrationTest).

Refs #2071

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

It unconditionally imported Jetty-specific classes
(org.eclipse.jetty.compression.server.CompressionHandler/CompressionConfig,
org.springframework.boot.jetty.ConfigurableJettyWebServerFactory), so a
hypothetical deployment without spring-boot-starter-jetty (Tomcat,
Undertow, or a test slice that doesn't pull it in) would fail to start
with a NoClassDefFoundError merely loading this configuration class,
rather than gracefully doing nothing.

Add @ConditionalOnClass on both types this class touches, matching the
project's own convention (see JedisClusterBucket4jConfiguration). Add
VSCodeGalleryCompressionConfigTest, using ApplicationContextRunner +
FilteredClassLoader, to prove the guard actually skips the
configuration when Jetty's compression support isn't on the classpath,
and still registers it when it is.

Refs #2071

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

VSCodeGalleryCompressionTest already proves the compression mechanism
works, but against a synthetic servlet, not the real application.

Add VSCodeGalleryExtensionQueryCompressionIntegrationTest: boots the
full app (real embedded Jetty, real VSCodeGalleryContentTypeAdvice and
VSCodeGalleryCompressionConfig wiring, real database-backed search) and
sends an actual HTTP request to /vscode/gallery/extensionquery with
server.compression.mime-types deliberately left at the bare
application/json a Spring Boot 3-era config would have had. Confirms
the response comes back Content-Encoding: gzip and genuinely decodes
to valid JSON, plus a sanity check that omitting Accept-Encoding
correctly gets an uncompressed response.

Refs #2071

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@netomi
netomi merged commit 2d670a1 into main Aug 18, 2026
5 checks passed
@netomi
netomi deleted the fix/compression-accept-header-params branch August 18, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants