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

Code before:
            val out = responseChannel as? ByteWriteChannel
            out?.flushAndClose()
        }
    }

    override suspend fun respondFromBytes(bytes: ByteArray) {
        // Note that it shouldn't set HttpHeaders.ContentLength even if we know it here,
        // because it should've been set by commitHeaders earlier
        val chunked = headers[HttpHeaders.TransferEncoding] == "chunked"

        if (responseMessageSent) return

        val message = responseMessage(chunked, bytes)
        responseChannel = when (message) {
            is LastHttpContent -> ByteReadChannel.Empty
            else -> ByteReadChannel(bytes)
        }
        responseMessage = message
        responseReady.setSuccess()
        responseMessageSent = true

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

    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()) {
            if (call is NettyApplicationCall) {
                (call as NettyApplicationCall).responseWriteJob.join()
            }
        }

        return channel
    }

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


Review comment (refactor):
The logic to await responseWriteJob for informational and NoContent statuses is duplicated in both sendResponse and responseChannel. Consider extracting this into a shared private method to reduce duplication and improve maintainability.
```suggestion
        awaitResponseWriteJobIfNeeded()
```

Code after:
            val out = responseChannel as? ByteWriteChannel
            out?.flushAndClose()
        }
    }

    override suspend fun respondFromBytes(bytes: ByteArray) {
        // Note that it shouldn't set HttpHeaders.ContentLength even if we know it here,
        // because it should've been set by commitHeaders earlier
        val chunked = headers[HttpHeaders.TransferEncoding] == "chunked"

        if (responseMessageSent) return

        val message = responseMessage(chunked, bytes)
        responseChannel = when (message) {
            is LastHttpContent -> ByteReadChannel.Empty
            else -> ByteReadChannel(bytes)
        }
        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()
            }
        }
    }