Is it reproducible with SwiftPM command-line tools: swift build, swift test, swift package etc?
Description
When SPM downloads a source archive from a package registry and receives a non-2xx status code (e.g., 403), the HTTP response body is discarded by URLSession's download task handler before SPM can read it. As a result, RegistryClient.parseError() cannot extract the RFC 9457 problem details, and the user sees only a generic error message (forbidden, client error 400: , etc.) — losing all context from the server about why the request failed.
This breaks RFC 9457 (Problem Details for HTTP APIs) support for the archive download endpoint, even though SPM has full RFC 9457 parsing infrastructure in place (parseError(), validates Content-Type: application/problem+json, validates Content-Version: 1).
It works correctly for metadata and manifest endpoints (which use data tasks and read the body), but fails for archive downloads (which use URLSessionDownloadTask).
Questions
- Is this intentional behavior, or a bug?
- If unintentional, would a PR be welcome that reads the temp file as response.body for non-2xx download responses before discarding it?
- Is there an alternate recommended way for registries to communicate errors during archive download?
Expected behavior
When a registry rejects an archive download (e.g., a security scanner has flagged the package as bad and won't allow it to be downloaded), it returns a non-2xx status code with an RFC 9457 problem details body explaining why:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Version: 1
{
"status": 403,
"title": "Bad Package",
"detail": "This package has been flagged as bad and cannot be downloaded. Contact your administrator for more information."
}
SPM should parse the body via parseError() and surface the detail field to the user:
error: 'vapor.vapor': failed downloading vapor.vapor version 3.0.0 source archive from http://example.com/: server error 403: This package has been flagged as bad and cannot be downloaded. Contact your administrator for more information.
This matches the existing behavior for metadata and manifest endpoints.
Actual behavior
The body is discarded. SPM shows a generic error with no information from the server:
error: 'vapor.vapor': failed downloading vapor.vapor version 3.0.0 source archive from http://example.com/: forbidden
The user has no way to learn why the package download was rejected.
Steps to reproduce
The temp file (containing the error body) is never read or moved, so it's automatically discarded by URLSession when the delegate returns.
Then in didCompleteWithError:
task.completionHandler(.success(response.response(body: nil)))
The body is explicitly set to nil, even though for non-2xx responses the temp file did contain the body.
This means in Sources/PackageRegistry/RegistryClient.swift's unexpectedStatusError():
if let error = try? response.parseError(decoder: self.jsonDecoder) {
return RegistryError.serverError(code: response.statusCode, details: error.detail)
}
// parseError() requires response.body, which is nil → falls through
switch response.statusCode {
case 403:
return RegistryError.forbidden // generic, no detail from server
case 400...499:
return RegistryError.clientError(
code: response.statusCode,
details: response.body.map { String(decoding: $0, as: UTF8.self) } ?? "" // body is nil → empty
)
Swift Package Manager version/commit hash
6.2.3
Swift & OS version (output of swift --version ; uname -a)
- Swift Package Manager: 6.2.3 (Swift 6.2.3, swiftlang-6.2.3.3.21)
- macOS: Darwin 25.5.0 (macOS 14.0 SDK)
- Architecture: arm64
Is it reproducible with SwiftPM command-line tools:
swift build,swift test,swift packageetc?swift build,swift test,swift packageetc.Description
When SPM downloads a source archive from a package registry and receives a non-2xx status code (e.g., 403), the HTTP response body is discarded by URLSession's download task handler before SPM can read it. As a result, RegistryClient.parseError() cannot extract the RFC 9457 problem details, and the user sees only a generic error message (forbidden, client error 400: , etc.) — losing all context from the server about why the request failed.
This breaks RFC 9457 (Problem Details for HTTP APIs) support for the archive download endpoint, even though SPM has full RFC 9457 parsing infrastructure in place (parseError(), validates Content-Type: application/problem+json, validates Content-Version: 1).
It works correctly for metadata and manifest endpoints (which use data tasks and read the body), but fails for archive downloads (which use URLSessionDownloadTask).
Questions
Expected behavior
When a registry rejects an archive download (e.g., a security scanner has flagged the package as bad and won't allow it to be downloaded), it returns a non-2xx status code with an RFC 9457 problem details body explaining why:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Version: 1
{
"status": 403,
"title": "Bad Package",
"detail": "This package has been flagged as bad and cannot be downloaded. Contact your administrator for more information."
}
SPM should parse the body via parseError() and surface the detail field to the user:
error: 'vapor.vapor': failed downloading vapor.vapor version 3.0.0 source archive from http://example.com/: server error 403: This package has been flagged as bad and cannot be downloaded. Contact your administrator for more information.
This matches the existing behavior for metadata and manifest endpoints.
Actual behavior
The body is discarded. SPM shows a generic error with no information from the server:
error: 'vapor.vapor': failed downloading vapor.vapor version 3.0.0 source archive from http://example.com/: forbidden
The user has no way to learn why the package download was rejected.
Steps to reproduce
The temp file (containing the error body) is never read or moved, so it's automatically discarded by URLSession when the delegate returns.
Then in didCompleteWithError:
task.completionHandler(.success(response.response(body: nil)))
The body is explicitly set to nil, even though for non-2xx responses the temp file did contain the body.
This means in Sources/PackageRegistry/RegistryClient.swift's unexpectedStatusError():
if let error = try? response.parseError(decoder: self.jsonDecoder) {
return RegistryError.serverError(code: response.statusCode, details: error.detail)
}
// parseError() requires response.body, which is nil → falls through
switch response.statusCode {
case 403:
return RegistryError.forbidden // generic, no detail from server
case 400...499:
return RegistryError.clientError(
code: response.statusCode,
details: response.body.map { String(decoding: $0, as: UTF8.self) } ?? "" // body is nil → empty
)
Swift Package Manager version/commit hash
6.2.3
Swift & OS version (output of
swift --version ; uname -a)