Maven registry: RFC 9457 - Problem Details support
Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.
🔥 Problem
The GitLab Maven package registry cannot communicate meaningful error messages to Maven clients (mvn, gradle, sbt).
Currently, all error responses use Content-Type: application/json with a body like {"message": "403 Forbidden - Package protected."}. Maven clients discard this body entirely — they only read the HTTP status code and reason phrase. Users see generic output like:
[ERROR] Could not transfer artifact com.example:foo:pom:1.0 from/to gitlab (https://gitlab.example.com/...):
status code: 403, reason phrase: Forbidden (403)On HTTP/2 connections (RFC 9113), it's even worse — reason phrases don't exist, so users see only:
[ERROR] Could not transfer artifact com.example:foo:pom:1.0 from/to gitlab (https://gitlab.example.com/...):
HTTP Status: 403This means protected package errors, permission issues, size limit violations, and validation failures all produce the same unhelpful output. Users have no way to understand why an operation failed without inspecting server logs.
💡 Solution
Adopt RFC 9457 — Problem Details for HTTP APIs for Maven package registry error responses.
When enabled, all 4xx/5xx responses from Maven endpoints will return:
Content-Type: application/problem+json(exact value, no parameters)- A JSON body with
type,status,title, anddetailfields
🔍 Example
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "about:blank",
"status": 403,
"title": "Forbidden",
"detail": "Package protected."
}Maven 3.9.11+ users will then see:
[ERROR] Could not transfer artifact com.example:foo:pom:1.0 from/to gitlab (https://gitlab.example.com/...):
RFC9457Payload {type=about:blank, status=403, title='Forbidden', detail='Package protected.'}⚙️ Design details
- Use
about:blankfor thetypefield per RFC 9457 section 4.2.1 (standard HTTP errors). Custom URIs can be added later. - Feature-flagged as
maven_problem_details_errors(default disabled for gradual rollout). - The
detailfield MUST NOT leak internal infrastructure details (hostnames, stack traces, etc.).
🛡️ Backward compatibility
This change is fully backward-compatible:
- Older Maven clients (pre-3.9.11) ignore the response body on errors entirely — they only read the status code and reason phrase. Changing the body format and Content-Type has zero impact.
- Gradle 8.x behaves the same way — ignores error bodies.
- sbt/Coursier ignores error bodies for resolution (GET). For publish (PUT), sbt displays up to 1024 chars of the response body as plain text — the RFC 9457 JSON will appear as raw JSON, which is acceptable.
🔗 Client compatibility matrix
📡 How Maven clients handle HTTP errors
There are three mechanisms a server can use to communicate error information:
- HTTP status code — always available, all clients use it
- HTTP/1.1 reason phrase — deprecated in HTTP/1.1 (RFC 9112), absent in HTTP/2 (RFC 9113)
- RFC 9457 Problem Details — structured JSON body with
Content-Type: application/problem+json
☕ Maven (Apache Maven Resolver)
Maven Resolver supports RFC 9457 since version 1.9.24 (MRESOLVER-600), backported from 2.0.2.
When the server returns Content-Type: application/problem+json, Maven Resolver:
- Detects the content type via exact string comparison (
"application/problem+json".equals(contentType)) - Reads and parses the body as JSON into
type,status,title,detail,instance - Throws
HttpRFC9457Exceptionwith the structured payload - User sees the details in the
"Could not transfer artifact..."error output
Critical implementation detail: Content-Type MUST be exactly application/problem+json with no parameters. application/problem+json; charset=utf-8 will NOT match due to exact string comparison in RFC9457Reporter. This is a known limitation in Maven Resolver.
Without RFC 9457, Maven falls back to status code + reason phrase. On HTTP/2 (no reason phrases), users see only the numeric code.
| Maven Version | Resolver Version | Transport | RFC 9457 Support | Error Display |
|---|---|---|---|---|
| 3.8.x and earlier | 1.6.x-1.8.x | Wagon (default) | No | Status code + reason phrase |
| 3.9.0 - 3.9.10 | 1.9.0-1.9.23 | HTTP transport | No | Status code + reason phrase |
| 3.9.11+ | 1.9.24+ | HTTP transport | Yes | RFC 9457 detail field displayed |
| 4.0.0-beta-5+ | 2.0.2+ | Apache/JDK transport | Yes (GET only with JDK transport) | RFC 9457 detail field displayed |
Maven Wagon (legacy): Never reads the response body on errors. Calls EntityUtils.consumeQuietly() and constructs messages from URL, status code, reason phrase, and proxy info only. No RFC 9457 support.
JDK transport gaps (Maven 4): RFC 9457 is only checked during GET operations, not PUT or PEEK. PUT operations use HttpResponse.BodyHandlers.discarding().
References:
- MRESOLVER-600 - RFC 9457 implementation
- apache/maven-resolver#576 - implementation PR
- apache/maven#2454 - Maven 4 RC-3 shows "HTTP Status: 500" with no body/reason
🐘 Gradle
Gradle does NOT support RFC 9457 today. Error display uses status code + reason phrase.
RFC 9457 support is in progress for Gradle 9.x (gradle/gradle#36203), citing MRESOLVER-600 as prior art.
| Gradle Version | RFC 9457 Support | Error Display |
|---|---|---|
| Current (8.x) | No | Status code + reason phrase |
| 9.x (planned) | Planned | RFC 9457 detail field |
🛠️ sbt / Coursier
| Operation | Client | RFC 9457 Support | Error Display |
|---|---|---|---|
| Resolution (GET) | Coursier | No | Status code only - body and reason phrase discarded |
| Publish (PUT) | Ivy transport | No (reads plain text) | Up to 1024 chars of response body as plain text (sbt#8537) |
Coursier's Downloader.scala (doDownload method) immediately returns error objects based on status code without reading the response body or reason phrase. The error stream is explicitly closed without reading. No feature request for RFC 9457 exists.
For sbt publish: the RFC 9457 JSON body will be displayed as raw JSON. The detail field should be placed early and be human-readable even without JSON parsing.
📊 Summary
| Client | GET/HEAD errors | PUT errors |
|---|---|---|
| Maven 3.9.11+ | RFC 9457 parsed and displayed | RFC 9457 parsed (Apache transport) |
| Maven 3.9.10 and earlier | Status code + reason phrase only | Status code + reason phrase only |
| Gradle (current 8.x) | Status code + reason phrase only | Status code + reason phrase only |
| Gradle 9.x (planned) | RFC 9457 planned | RFC 9457 planned |
| sbt/Coursier | Status code only | Plain-text body shown (up to 1024 chars) |
🔧 Implementation approach
Create a helper module API::Helpers::Packages::Maven::ProblemDetails that overrides render_structured_api_error! — the single method all error helpers flow through:
forbidden!("reason") / not_found!("Package") / bad_request!("reason") / ...
-> render_api_error_with_reason! or render_api_error!
-> render_structured_api_error!({ 'message' => "4xx ... - reason" }, status)
-> error!(hash, status, header) # Grape's methodThe override:
- Checks the
maven_problem_details_errorsfeature flag - Only transforms 4xx/5xx responses (not 2xx like
no_content!which also flows through this path) - Extracts a clean
detailfrom the legacy message string by stripping the"NNN StatusPhrase - "prefix - Sets
Content-Type: application/problem+jsonand constructs the RFC 9457 body - When the flag is disabled, calls
super— zero behavioral change
Include this module in the Maven API classes only — no impact on other API endpoints.
📁 Impacted files
🆕 New files
| File | Purpose |
|---|---|
config/feature_flags/development/maven_problem_details_errors.yml |
Feature flag definition |
lib/api/helpers/packages/maven/problem_details.rb |
Helper module overriding render_structured_api_error! |
spec/lib/api/helpers/packages/maven/problem_details_spec.rb |
Unit tests for the helper |
✏️ Modified files
| File | Change |
|---|---|
lib/api/maven_packages.rb |
Include the helper module |
ee/lib/api/dependency_proxy/packages/maven.rb |
Include the helper module |
ee/lib/api/virtual_registries/packages/maven/endpoints.rb |
Include the helper module |
spec/requests/api/maven_packages_spec.rb |
Add RFC 9457 shared examples + feature flag contexts |
ee/spec/requests/api/maven_packages_spec.rb |
Same |
ee/spec/requests/api/dependency_proxy/packages/maven_spec.rb |
Same |
🚨 Error scenarios in lib/api/maven_packages.rb
| Status | Trigger | Expected detail |
|---|---|---|
| 400 | ActiveRecord::RecordInvalid |
Validation message |
| 400 | bad_request!('File is too large') |
"File is too large" |
| 401 | unauthorized! (no current_user) |
(none) |
| 403 | forbidden! (path doesn't exist) |
(none) |
| 403 | forbidden!(result.errors.first) (protected package) |
"Package protected." |
| 404 | not_found!('Package') |
"Package not found" |
| 404 | not_found!('Group') |
"Group not found" |
| 404 | not_found!('Project') |
"Project not found" |
| 409 | conflict! (SHA verification mismatch) |
(none) |
| 422 | unprocessable_entity! (FIPS mode MD5 rejection) |
(none) |
✅ Verification plan
🌐 curl verification (GDK)
# Enable feature flag
# Rails console: Feature.enable(:maven_problem_details_errors)
# 401 - no token
curl -v "http://gdk.test:3000/api/v4/projects/1/packages/maven/com/example/foo/1.0/foo.jar"
# Expected: {"type":"about:blank","status":401,"title":"Unauthorized"}
# 403 - protected package upload
curl -v -X PUT -H "Private-Token: $TOKEN" \
"http://gdk.test:3000/api/v4/projects/1/packages/maven/com/example/protected/1.0/foo.jar/authorize"
# Expected: {"type":"about:blank","status":403,"title":"Forbidden","detail":"Package protected."}
# 404 - nonexistent package
curl -v -H "Private-Token: $TOKEN" \
"http://gdk.test:3000/api/v4/projects/1/packages/maven/com/example/nonexistent/1.0/foo.jar"
# Expected: {"type":"about:blank","status":404,"title":"Not Found","detail":"Package not found"}
# CRITICAL: verify Content-Type has no parameters
curl -sI -H "Private-Token: invalid" \
"http://gdk.test:3000/api/v4/projects/1/packages/maven/com/example/foo/1.0/foo.jar" \
| grep -i content-type
# MUST be exactly: Content-Type: application/problem+json
# MUST NOT be: Content-Type: application/problem+json; charset=utf-8📦 Client versions to test
| Client | Version | Purpose |
|---|---|---|
| Maven | 3.9.11+ | First version with RFC 9457 support - primary test target |
| Maven | 3.8.8 | Pre-RFC 9457 baseline - verify backward compatibility |
| Maven | 4.0.0+ (if available) | New resolver with JDK transport |
| Gradle | current 8.x | Verify no breakage (ignores error body) |
| sbt | current | Verify no breakage for resolution; check publish raw body display |
🧪 Test scenarios
For each client version, test with feature flag enabled and disabled:
- Authentication failure (401) - wrong/missing token; verify error detail displayed (Maven 3.9.11+) or graceful fallback (older)
- Permission denied (403) - token without package read permission
- Protected package (403) - attempt upload to protected package; verify "Package protected." detail
- Package not found (404) - resolve nonexistent artifact
- Upload too large (400) - upload exceeding size limit
- SHA mismatch (409) - upload with mismatched checksum
- FIPS MD5 rejection (422) - if applicable
🔮 Expected output
Maven 3.9.11+ with flag enabled:
[ERROR] Could not transfer artifact com.example:foo:pom:1.0 from/to gitlab (http://gdk.test:3000/...):
RFC9457Payload {type=about:blank, status=403, title='Forbidden', detail='Package protected.'}Maven 3.8.x with flag enabled (backward compat):
[ERROR] Could not transfer artifact com.example:foo:pom:1.0 from/to gitlab (http://gdk.test:3000/...):
status code: 403, reason phrase: Forbidden (403)Old clients ignore the application/problem+json body entirely and fall back to status code + reason phrase — no breakage.