Repository: ktorio/ktor
File: ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/NettyApplicationResponse.kt (Kotlin)

Code before:
            is LastHttpContent -> ByteReadChannel.Empty
            else -> ByteReadChannel(bytes)
        }
        responseMessage = message
        responseReady.setSuccess()
        responseMessageSent = true

        if (isInfoOrNoContentStatus()) {
            sendCompleted.await()
        }
    }

    internal fun isInfoOrNoContentStatus(): Boolean {
        val status = status()
        return (status != null) && (status == HttpStatusCode.NoContent || (status.value >= 100 && status.value < 200))
    }

    override suspend fun responseChannel(): ByteWriteChannel {
        val channel = ByteChannel()
        val chunked = headers[HttpHeaders.TransferEncoding] == "chunked"
        sendResponse(chunked, content = channel)

        if (isInfoOrNoContentStatus()) {
            sendCompleted.await()
        }

        return channel
    }

    override suspend fun respondNoContent(content: OutgoingContent.NoContent) {
        respondFromBytes(EmptyByteArray)
    }

    protected abstract fun responseMessage(chunked: Boolean, last: Boolean): Any

    /**
     * Returns http response object with [data] content
     */
    protected open fun responseMessage(chunked: Boolean, data: ByteArray): Any = responseMessage(chunked, true)

    /**
     * Returns http trailer message
     */
    internal open fun prepareTrailerMessage(): Any? {
        return null
    }

    internal fun sendResponse(chunked: Boolean = true, content: ByteReadChannel) {
        if (responseMessageSent) return

        responseChannel = content

Review comment (suggestion):
Looks like you could avoid the extra deferred here and just use the call's response job:

```
(call as NettyApplicationCall).responseWriteJob.join()
```

Maybe change the constructor arg to `val engineCall: NettyApplicationCall` to avoid the cast.

Is the logic here necessary for the "info / no content"?  I think we could drop the if clause to avoid any unknown scenarios where the netty engine might manipulate headers on it's own, and it would seem best practice to wait for netty to say the response is ready before we continue onto callbacks.

Code after:
        responseMessage = message
        responseReady.setSuccess()
        responseMessageSent = true

        awaitProcessingResponseIfInfoOrNoContent()
    }

    override suspend fun responseChannel(): ByteWriteChannel {
        val channel = ByteChannel()
        val chunked = headers[HttpHeaders.TransferEncoding] == "chunked"
        sendResponse(chunked, content = channel)

        awaitProcessingResponseIfInfoOrNoContent()

        return channel
    }

    /**
     * Await the [NettyApplicationCall.responseWriteJob] to complete if the response status is informational or 204.
     * Netty discards certain headers of such responses, so we have to wait for Netty to finish that,
     * in order to avoid the race condition.
     */
    private suspend fun awaitProcessingResponseIfInfoOrNoContent() {
        val status = status()

        if (status != null) {
            val infoOrNoContent = status == HttpStatusCode.NoContent || (status.value >= 100 && status.value < 200)

            if (infoOrNoContent && call is NettyApplicationCall) {
                (call as NettyApplicationCall).responseWriteJob.join()
            }
        }
    }

    override suspend fun respondNoContent(content: OutgoingContent.NoContent) {
        respondFromBytes(EmptyByteArray)
    }

    protected abstract fun responseMessage(chunked: Boolean, last: Boolean): Any

    /**
     * Returns http response object with [data] content
     */
    protected open fun responseMessage(chunked: Boolean, data: ByteArray): Any = responseMessage(chunked, true)

    /**
     * Returns http trailer message
     */
    internal open fun prepareTrailerMessage(): Any? {
        return null
    }