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

Local Debugging: LambdaHttpServer Drops All But Last Inbound Body Chunk #481

Open
macserv opened this issue Feb 19, 2025 · 1 comment
Open
Assignees
Labels
kind/bug Feature doesn't work as expected.
Milestone

Comments

@macserv
Copy link

macserv commented Feb 19, 2025

Expected behavior

When handling the connection, all body data chunks should be collected into the requestBody buffer from the inbound stream for processing.

Actual behavior

The requestBody buffer is repeatedly reset to the last chunk received, so that only the last chunk is sent for processing.

Steps to reproduce

  1. Create a sample StreamingLambdaHandler and run it locally.
  2. Feed a decent amount of data to the invoke endpoint (my test data is around 260KB, but I don't think it needs to be anywhere near that large to get split into chunks).
  3. Log the event data.
  4. Observe that only the last chunk of body data is processed.

If possible, minimal yet complete reproducer code (or URL to code)

Sample StreamingLambdaHandler

@main
struct RawInputHandler: StreamingLambdaHandler
{
    func handle(_ event: ByteBuffer, responseWriter: some LambdaResponseStreamWriter, context: LambdaContext) async throws
    {
        context.logger.info("Raw JSON:\n\( String(data: Data(buffer: event), encoding: .utf8)! )")
        try await responseWriter.finish()
    }

    static func main() async throws { try await LambdaRuntime(handler: Self()).run() }
}

Invocation with curl

cat ./TestData.json | curl --json '@-' 'http://localhost:7000/invoke'

Root Cause

The issue seems to reside in the handleConnection() function in /Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift:163–204:

    /// This method handles individual TCP connections
    private func handleConnection(
        channel: NIOAsyncChannel<HTTPServerRequestPart, HTTPServerResponsePart>
    ) async {

        var requestHead: HTTPRequestHead!
        var requestBody: ByteBuffer?

        // Note that this method is non-throwing and we are catching any error.
        // We do this since we don't want to tear down the whole server when a single connection
        // encounters an error.
        do {
            try await channel.executeThenClose { inbound, outbound in
                for try await inboundData in inbound {
                    if case .head(let head) = inboundData {
                        requestHead = head
                    }
                    if case .body(let body) = inboundData {
                        requestBody = body
                    }
                    if case .end = inboundData {
                        precondition(requestHead != nil, "Received .end without .head")
                        // process the request
                        let response = try await self.processRequest(
                            head: requestHead,
                            body: requestBody
                        )
                        // send the responses
                        try await self.sendResponse(
                            response: response,
                            outbound: outbound
                        )

                        requestHead = nil
                        requestBody = nil
                    }
                }
            }
        } catch {
            logger.error("Hit error: \(error)")
        }
    }

Within the closure for executeThenClose, each inboundData chunk arrives, and if it is determined to be .body data, the requestBody variable is reassigned to the newest chunk. When the .end case is reached, only the last chunk of body data has been captured for processing.

Recommendation

An easy change would be to replace requestBody = body with requestBody.setOrWriteImmutableBuffer(body). This would allow the body chunks to accumulate in the requestBody buffer.

I've not spent much time evaluating potential edge cases, but the following modification works without issue in my own environment:

    /// This method handles individual TCP connections
    ///
    /// - Note: This method is non-throwing and we are catching any error.  This
    ///     was done so that we don't tear down the whole server when a single
    ///     connection encounters an error.
    private func handleConnection(channel: NIOAsyncChannel<HTTPServerRequestPart, HTTPServerResponsePart>) async {
        var requestHead: HTTPRequestHead!
        var requestBody: ByteBuffer?

        do {
            try await channel.executeThenClose { inbound, outbound in
                for try await inboundData in inbound {
                    switch inboundData {
                        case .head(let head): requestHead = head
                        case .body(let body): requestBody.setOrWriteImmutableBuffer(body)
                        case .end:
                            precondition(requestHead != nil, "Received .end without .head")

                            // Process the request and send the responses.
                            let response = try await self.processRequest(head: requestHead, body: requestBody)
                            try await self.sendResponse(response: response, outbound: outbound)

                            requestHead = nil
                            requestBody = nil
                    }
                }
            }
        } catch {
            logger.error("Hit error: \(error)")
        }
    }

swift-aws-lambda-runtime version

main@40e2291532fdc0cf2e54f285bf33dc37b740646f

Swift version

swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.9 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0
Darwin XYHY90YH44 24.3.0 Darwin Kernel Version 24.3.0: Thu Jan  2 20:24:23 PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6020 arm64

Amazon Linux 2 docker image version

6.0-amazonlinux2

Thanks

I'm tremendously grateful to this project's contributors for their time spent evaluating this issue, and for the great work they've done as a whole. As part of my efforts to promote Swift as a general-purpose language beyond it's iOS-app-shaped pigeonhole, the swift-aws-lambda-runtime package has allowed me to deliver a particularly simple and impactful demonstration of the benefits and possibilities afforded by the language.

@sebsto
Copy link
Contributor

sebsto commented Feb 27, 2025

Thank you for having reported this and the detailled report, including a possible solution.

I will test again now that #486 is merged.

@sebsto sebsto self-assigned this Feb 27, 2025
@sebsto sebsto added the kind/bug Feature doesn't work as expected. label Feb 27, 2025
@sebsto sebsto added this to the 2.0 milestone Feb 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/bug Feature doesn't work as expected.
Projects
None yet
Development

No branches or pull requests

2 participants