Repository: apple/swift-nio
File: Sources/NIOHTTP1/HTTPEncoder.swift (Swift)

Code before:
        self.configuration = configuration
    }

    public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
        switch self.unwrapOutboundIn(data) {
        case .head(var response):
            assert(!(response.headers.contains(name: "content-length") &&
                        response.headers[canonicalForm: "transfer-encoding"].contains("chunked"[...])),
                     "illegal HTTP sent: \(response) contains both a content-length and transfer-encoding:chunked")

            if self.configuration.automaticallySetFramingHeaders {
                self.isChunked = correctlyFrameTransportHeaders(hasBody: response.status.mayHaveResponseBody ? .yes : .no,
                                                                headers: &response.headers, version: response.version) == .chunked
            } else {
                self.isChunked = messageIsChunked(headers: response.headers, version: response.version)
            }

            writeHead(wrapOutboundOut: self.wrapOutboundOut, writeStartLine: { buffer in
                buffer.write(response: response)
            }, context: context, headers: response.headers, promise: promise)
        case .body(let bodyPart):
            if case let .byteBuffer(byteBuffer) = bodyPart, byteBuffer.readableBytes == 0 {
                // empty body parts will end the response which we don't want
                // this happens in llhttp which is a vendored C library transpiled from TypeScript
                // we don't want to change the upstream library and therefore handle this special case here
                break
            }
            writeChunk(wrapOutboundOut: self.wrapOutboundOut, context: context, isChunked: self.isChunked, chunk: bodyPart, promise: promise)
        case .end(let trailers):
            writeTrailers(wrapOutboundOut: self.wrapOutboundOut, context: context, isChunked: self.isChunked, trailers: trailers, promise: promise)
        }
    }
}

@available(*, unavailable)
extension HTTPResponseEncoder: Sendable {}

private extension ByteBuffer {
    private mutating func write(status: HTTPResponseStatus) {
        self.writeString(String(status.code))
        self.writeWhitespace()
        self.writeString(status.reasonPhrase)
    }

    mutating func write(response: HTTPResponseHead) {
        switch (response.version.major, response.version.minor, response.status) {
        // Optimization for HTTP/1.0
        case (1, 0, .custom(_, _)):
            self.writeStaticString("HTTP/1.0 ")
            self.write(status: response.status)
            self.writeStaticString("\r\n")

Review comment (bug):
This fix is incorrect and the analysis isn't right either.

Firstly, the analysis. Empty body parts _only_ end the response if we're using chunked encoding. That's because the empty body chunk signals the end of the body, and so is what we send in `writeTrailers`. This has nothing to do with llhttp, it's spec-behaviour.

Secondly, the fix. This leaks a promise on the outbound path. We must pass on a zero-length write down the pipeline to ensure the promise is preserved.

Code after:
                     "illegal HTTP sent: \(response) contains both a content-length and transfer-encoding:chunked")

            if self.configuration.automaticallySetFramingHeaders {
                self.isChunked = correctlyFrameTransportHeaders(hasBody: response.status.mayHaveResponseBody ? .yes : .no,
                                                                headers: &response.headers, version: response.version) == .chunked
            } else {
                self.isChunked = messageIsChunked(headers: response.headers, version: response.version)
            }

            writeHead(wrapOutboundOut: self.wrapOutboundOut, writeStartLine: { buffer in
                buffer.write(response: response)
            }, context: context, headers: response.headers, promise: promise)
        case .body(let bodyPart):
            writeChunk(wrapOutboundOut: self.wrapOutboundOut, context: context, isChunked: self.isChunked, chunk: bodyPart, promise: promise)
        case .end(let trailers):
            writeTrailers(wrapOutboundOut: self.wrapOutboundOut, context: context, isChunked: self.isChunked, trailers: trailers, promise: promise)
        }
    }
}

@available(*, unavailable)
extension HTTPResponseEncoder: Sendable {}

private extension ByteBuffer {
    private mutating func write(status: HTTPResponseStatus) {
        self.writeString(String(status.code))
        self.writeWhitespace()
        self.writeString(status.reasonPhrase)
    }

    mutating func write(response: HTTPResponseHead) {
        switch (response.version.major, response.version.minor, response.status) {
        // Optimization for HTTP/1.0
        case (1, 0, .custom(_, _)):
            self.writeStaticString("HTTP/1.0 ")
            self.write(status: response.status)
            self.writeStaticString("\r\n")
        case (1, 0, .continue):
            self.writeStaticString("HTTP/1.0 100 Continue\r\n")
        case (1, 0, .switchingProtocols):
            self.writeStaticString("HTTP/1.0 101 Switching Protocols\r\n")
        case (1, 0, .processing):
            self.writeStaticString("HTTP/1.0 102 Processing\r\n")
        case (1, 0, .ok):
            self.writeStaticString("HTTP/1.0 200 OK\r\n")
        case (1, 0, .created):
            self.writeStaticString("HTTP/1.0 201 Created\r\n")
        case (1, 0, .accepted):
            self.writeStaticString("HTTP/1.0 202 Accepted\r\n")
        case (1, 0, .nonAuthoritativeInformation):
            self.writeStaticString("HTTP/1.0 203 Non-Authoritative Information\r\n")