EmbeddedChannel not returning data to be read

Hi everyone,

I'm trying to use EmbeddedChannel to test a router for an HTTP server.

Based on this talk from @johannesweiss, I wrote a test like this

    let handler = HTTPHandler()

    let channel = EmbeddedChannel(handler: handler)

    try channel.writeInbound(HTTPServerRequestPart.head(HTTPRequestHead(version: http11, method: .GET, uri: "/testing/456")))
    try channel.writeInbound(HTTPServerRequestPart.body(ByteBuffer()))
    try channel.writeInbound(HTTPServerRequestPart.end(nil))

    XCTAssertEqual(try channel.readInbound(), HTTPServerResponsePart.head(HTTPResponseHead(version: http11, status: .ok)))
    XCTAssertEqual(try channel.readInbound(), HTTPServerResponsePart.body(IOData.byteBuffer(ByteBuffer(string: "The ID is 456"))))
    XCTAssertEqual(try channel.readInbound(), HTTPServerResponsePart.end(nil))
    XCTAssertNil(try channel.readInbound())

This test returns nil every time i call channel.readInbound(). I also threw in some asserts to see if the channel was active

    XCTAssert(channel.isActive)

and these assertions always fail, no matter where in the test I put them.

I ended up getting around this by adding the RecordingHandler from the WG github, and asserting against that:

    XCTAssertEqual(recorder.writes[0], HTTPServerResponsePart.head(HTTPResponseHead(version: http11, status: .ok)))
    XCTAssertEqual(recorder.writes[1], HTTPServerResponsePart.body(IOData.byteBuffer(ByteBuffer(string: "The ID is 456"))))
    XCTAssertEqual(recorder.writes[2], HTTPServerResponsePart.end(nil))

which worked.

What am I doing wrong with EmbeddedChannel?

There are a few things going wrong here. Some are easy to diagnose from this description, some are harder.

Firstly, what does HTTPHandler do?

Secondly, as for isActive being false, to activate EmbeddedChannel you need to call connect on it (even if it’s for a server!)

HTTPHandler is a ChannelInboundHandler reads 3 HTTPServerRequestPart objects from its InboundIn, and then writes out 3 HTTPServerResponsePart objects, flushing the last one, and then closing channel when the promises of all three writes are done.

// ------✂-------

let part = HTTPServerResponsePart.head(head)

let future = channel.write(part)

var buffer = channel.allocator.buffer(capacity: 100)
buffer.writeBytes(try! response.body())

let bodyPart = HTTPServerResponsePart.body(.byteBuffer(buffer))
let future2 = channel.write(bodyPart)

let endPart = HTTPServerResponsePart.end(nil)
let future3 = channel.writeAndFlush(endPart)

_ = future.and(future2).and(future3)
    .flatMap({ (_) -> EventLoopFuture<Void> in
        return channel.close()
    })

So if you write out parts, you don’t want readInbound, you want readOutbound in your test. readInbound will show us any data that’s transformed by the handler, not data produced by it.

Ah, got it. So that makes it return a value instead of nil, but they now return IOData.byteBuffer objects where the inner byte buffer is always 0 bytes.

let part1: IOData? = try channel.readOutbound()
let part2: IOData? = try channel.readOutbound()
let part3: IOData? = try channel.readOutbound()
let part4: IOData? = try channel.readOutbound()

guard case let .byteBuffer(buffer1) = part1 else { XCTFail(); return }
guard case let .byteBuffer(buffer2) = part2 else { XCTFail(); return }
guard case let .byteBuffer(buffer3) = part3 else { XCTFail(); return }
XCTAssertEqual(buffer1.readableBytesView.count, 0)
XCTAssertEqual(buffer2.readableBytesView.count, 0)
XCTAssertEqual(buffer3.readableBytesView.count, 0)

XCTAssertNil(part4)

(All these assertions succeed.)

Is there a reason they're returning 0 length byte buffers instead of HTTPServerResponsePart?

They return a byteBuffer because you explicitly request one using let part1: IOData, right? I think it might be sufficient to just type it as HTTPServerResponsePart instead? (checkout the readOutbound method signature: https://apple.github.io/swift-nio/docs/current/NIO/Classes/EmbeddedChannel.html#/s:3NIO15EmbeddedChannelC12readOutbound2asxSgxm_tKlF)

Disclaimer: haven't used the embedded channel yet.

If you type it as HTTPServerResponsePart?, it throws a WrongTypeError, sadly.

Are there any other handlers in the pipeline?

Yup, that was it! The recorder was still in the pipeline. Thank you!

You are welcome, please don’t hesitate to ask any further questions that might come up.