Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Two handy extensions to work with HTTPClientResponse body #736

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

makoni
Copy link

@makoni makoni commented Apr 8, 2024

Two extensions for HTTPClientResponse to avoid boilerplate code.

Reading a response data currently:

let expectedBytes = headers.first(name: "content-length").flatMap(Int.init) ?? 1024 * 1024
var bytes = try await body.collect(upTo: expectedBytes)
if let data = bytes.readData(length: bytes.readableBytes) {
    // handle data
}

With the extension:

if let data = try await response.data(upTo: 1024*1024) {
    // handle data
}

Additionally an extension for bytes. Currently:

let expectedBytes = headers.first(name: "content-length").flatMap(Int.init) ?? 1024 * 1024
var bytes = try await body.collect(upTo: expectedBytes)

With the extension:

var bytes = try await response.bytes(upTo: 1024*1024)

Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Generally these look really good! Can you add a couple of tests for them as well?

let expectedBytes = headers
.first(name: "content-length")
.flatMap(Int.init) ?? 1024 * 1024
return try await body.collect(upTo: expectedBytes)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I like this helper, but I think it might be a bit better as a function. In particular I'm very reluctant to let users avoid providing the default maximum body size: just hard-coding this to 1MB is probably going to cause some confusion.

Copy link
Author

Choose a reason for hiding this comment

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

I'm not really sure about a default value, but at the same time I found that sometimes there's not "content-length" header. I'm working on a small client lib for CouchDB which has HTTP API. And some of its responses doesn't have that header. But with some default value it works fine.

Do you have any other ideas how to avoid hard-coding?

Copy link
Author

@makoni makoni Apr 9, 2024

Choose a reason for hiding this comment

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

I think I found a solution:

public func bytes(upTo maxBytes: Int? = nil) async throws -> ByteBuffer {
    if let expectedBytes = maxBytes ?? self.headers.first(name: "content-length").flatMap(Int.init) {
        return try await self.body.collect(upTo: expectedBytes)
    }

    var data = [UInt8]()
    for try await var buffer in self.body {
        data = data + (buffer.readBytes(length: buffer.readableBytes) ?? [])
    }

    return ByteBuffer(bytes: data)
}

Copy link
Author

Choose a reason for hiding this comment

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

Added 2 new tests

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure we should automatically depend on "content-length" here. A malicious client may send just peta bytes of data, which can bring down the client.

Copy link
Author

Choose a reason for hiding this comment

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

If you're getting an image from some URL you probably don't know is its size 1mb or 20mb. If you'll pass too small upTo value there will be a crash.

Here's an example that gives Swift/ErrorType.swift:200: Fatal error: Error raised at top level: NIOCore.NIOTooManyBytesError() with 1.21.0:

let request = HTTPClientRequest(url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png")
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
let buffer = try await response.body.collect(upTo: 10)

If you don't know the data size it makes sense to provide some big value (like 1012 * 1012 * 100 for 100MB for example), or to use "content-length" header that tells you the size of the data.

If you don't trust the source it's still possible to provide upTo param in the public func bytes(upTo maxBytes: Int? = nil) async throws -> ByteBuffer method above.

Do you have a better idea? 🙂

Copy link
Collaborator

@Lukasa Lukasa Apr 12, 2024

Choose a reason for hiding this comment

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

A better version might be to provide a maximum value the user is willing to accept here, and then use content-length or the maximum value. We can then throw errors if the content-length is larger than the max.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks Lukasa. I've updated the PR, made upTo param required for both helper functions as you suggested. If the content-length is larger than the max there will be an error thrown. Also updated the tests.

/// Response body as `Data`.
public var data: Data? {
get async throws {
var bytes = try await bytes
Copy link
Collaborator

Choose a reason for hiding this comment

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

Mild nit: our style is to use self. wherever relevant.

Copy link
Author

Choose a reason for hiding this comment

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

Done

@swift-server swift-server deleted a comment from Mizhkaa Apr 9, 2024
@makoni makoni requested a review from Lukasa April 9, 2024 11:25
/// - Parameter maxBytes: The maximum number of bytes this method is allowed to accumulate.
/// - Returns: Bytes collected over time
public func bytes(upTo maxBytes: Int) async throws -> ByteBuffer {
let expectedBytes = self.headers.first(name: "content-length").flatMap(Int.init) ?? maxBytes
Copy link
Collaborator

Choose a reason for hiding this comment

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

This shouldn't just blindly trust the content-length: it should reject values of the content length above the upTo value.

/// Response body as `Data`.
/// - Parameter maxBytes: The maximum number of bytes this method is allowed to accumulate.
/// - Returns: Bytes collected over time
public func data(upTo maxBytes: Int) async throws -> Data? {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same note here.

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.

3 participants