Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ All notable changes to this project will be documented in this file. Take a look
* `LCPService.init` now requires an explicit `deviceName` parameter. We recommend passing `UIDevice.current.name`. See [the migration guide](docs/Migration%20Guide.md).
* `LCPDialogAuthentication` no longer takes a `sender` view controller. It now presents its passphrase dialog through a new `LCPDialogAuthenticationDelegate` that you implement and retain for the lifetime of the authentication. See [the Readium LCP guide](docs/Guides/Readium%20LCP.md) and [the migration guide](docs/Migration%20Guide.md).

### Fixed

#### Shared

* [#653](https://github.com/readium/swift-toolkit/issues/653) `DefaultHTTPClient` now sends `Accept-Encoding: identity` with byte range requests. Previously, `URLSession` transparently negotiated a compressed (e.g. gzip) response, for which servers usually cannot serve byte ranges, causing streaming to fail with `HTTPError.rangeNotSupported` even though the server supports ranges.

### Removed

* The deprecated `ReadiumAdapterGCDWebServer` and `ReadiumAdapterLCPSQLite` adapter packages have been removed.
Expand Down
10 changes: 10 additions & 0 deletions Sources/Shared/Toolkit/HTTP/DefaultHTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,16 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
request.userAgent = userAgent
}

// `URLSession` transparently negotiates compressed responses (e.g.
// gzip), but servers usually cannot serve byte ranges of a compressed
// representation and may omit the `Accept-Ranges` header in that case.
// Requesting the identity encoding ensures the server advertises and
// serves byte ranges properly.
// See https://github.com/readium/swift-toolkit/issues/653
if request.hasHeader("Range"), !request.hasHeader("Accept-Encoding") {

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.

Nothing wrong, might be better if this was in HTTPRequest as a mutating function.

request.headers["Accept-Encoding"] = "identity"
}

log(.info, request)

let taskDelegate = TaskDelegate(
Expand Down
57 changes: 57 additions & 0 deletions Tests/SharedTests/Toolkit/HTTP/DefaultHTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,63 @@ struct DefaultHTTPClientTests {
}
}

@Test("Range request sends Accept-Encoding: identity to prevent transparent compression")
func rangeRequestSendsIdentityAcceptEncoding() async {
let receivedAcceptEncoding = Capture<String?>(nil)

let client = makeClient { request in
receivedAcceptEncoding.value = request.value(forHTTPHeaderField: "Accept-Encoding")
return .success(
statusCode: 206,
headers: ["Accept-Ranges": "bytes"],
body: Data("partial".utf8)
)
}

var httpRequest = HTTPRequest(url: makeURL())
httpRequest.setRange(0 ..< 7)
_ = await client.fetch(httpRequest)

#expect(receivedAcceptEncoding.value == "identity")
}

@Test("Range request preserves a custom Accept-Encoding header")
func rangeRequestPreservesCustomAcceptEncoding() async {
let receivedAcceptEncoding = Capture<String?>(nil)

let client = makeClient { request in
receivedAcceptEncoding.value = request.value(forHTTPHeaderField: "Accept-Encoding")
return .success(
statusCode: 206,
headers: ["Accept-Ranges": "bytes"],
body: Data("partial".utf8)
)
}

var httpRequest = HTTPRequest(url: makeURL(), headers: ["Accept-Encoding": "gzip"])
httpRequest.setRange(0 ..< 7)
_ = await client.fetch(httpRequest)

#expect(receivedAcceptEncoding.value == "gzip")
}

@Test("Regular request does not force the Accept-Encoding header")
func regularRequestDoesNotForceAcceptEncoding() async {
let receivedAcceptEncoding = Capture<String?>(nil)

let client = makeClient { request in
receivedAcceptEncoding.value = request.value(forHTTPHeaderField: "Accept-Encoding")
return .success()
}

_ = await client.fetch(HTTPRequest(url: makeURL()))

// `URLSession` may transparently add its own `Accept-Encoding`
// (e.g. gzip) at a lower level, so we only check that the client
// did not force the identity encoding.
#expect(receivedAcceptEncoding.value != "identity")
}

@Test("Open-ended setRange omits upper bound in Range header")
func openEndedRangeRequest() async {
let receivedRange = Capture<String?>(nil)
Expand Down