All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.pellet.server.codec.http.HTTPRequestHandler.kt Maven / Gradle / Ivy

There is a newer version: 0.0.16
Show newest version
package dev.pellet.server.codec.http

import dev.pellet.logging.PelletLogElements
import dev.pellet.logging.error
import dev.pellet.logging.info
import dev.pellet.logging.logElements
import dev.pellet.logging.pelletLogger
import dev.pellet.server.CloseReason
import dev.pellet.server.PelletServerClient
import dev.pellet.server.buffer.PelletBufferPooling
import dev.pellet.server.codec.CodecHandler
import dev.pellet.server.metrics.PelletTimer
import dev.pellet.server.responder.http.PelletHTTPResponder
import dev.pellet.server.responder.http.PelletHTTPRouteContext
import dev.pellet.server.routing.http.HTTPRouteResponse
import dev.pellet.server.routing.http.HTTPRouting
import java.time.Instant
import java.time.ZoneOffset.UTC
import java.time.format.DateTimeFormatterBuilder
import java.time.format.TextStyle
import java.time.temporal.ChronoField

internal class HTTPRequestHandler(
    private val client: PelletServerClient,
    private val router: HTTPRouting,
    private val pool: PelletBufferPooling,
    private val logRequests: Boolean
) : CodecHandler {

    private val timer = PelletTimer()
    private val logger = pelletLogger()

    companion object {

        val commonDateFormat = DateTimeFormatterBuilder()
            .appendValue(ChronoField.DAY_OF_MONTH, 2)
            .appendLiteral('/')
            .appendText(ChronoField.MONTH_OF_YEAR, TextStyle.SHORT)
            .appendLiteral('/')
            .appendValue(ChronoField.YEAR, 4)
            .appendLiteral(':')
            .appendValue(ChronoField.HOUR_OF_DAY, 2)
            .appendLiteral(':')
            .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
            .appendLiteral(':')
            .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
            .appendLiteral(' ')
            .appendOffset("+HHMM", "0000")
            .toFormatter()!!

        const val requestMethodKey = "request.method"
        const val requestUriKey = "request.uri"
        const val responseCodeKey = "response.code"
        const val responseDurationKey = "response.duration_ms"
    }

    override suspend fun handle(output: HTTPRequestMessage) {
        timer.reset()
        val context = PelletHTTPRouteContext(output, client)
        val responder = PelletHTTPResponder(client, pool)
        val route = router.route(output)
        if (route == null) {
            val response = HTTPRouteResponse.Builder()
                .notFound()
                .build()
            respond(output, response, responder, timer)
        } else {
            val routeResult = runCatching {
                route.handler.handle(context)
            }
            if (routeResult.isFailure) {
                logger.error(routeResult.exceptionOrNull()) { "failed to handle request" }
                val response = HTTPRouteResponse.Builder()
                    .internalServerError()
                    .build()
                respond(output, response, responder, timer)
            } else {
                respond(output, routeResult.getOrThrow(), responder, timer)
            }
        }

        val connectionHeader = output.headers.getSingleOrNull(HTTPHeaderConstants.connection)
        handleConnectionHeader(connectionHeader)
        output.release(pool)
    }

    private suspend fun respond(
        request: HTTPRequestMessage,
        response: HTTPRouteResponse,
        responder: PelletHTTPResponder,
        timer: PelletTimer
    ) {
        val message = mapRouteResponseToMessage(response)
        val requestDuration = timer.markAndReset()
        if (logRequests) {
            val elements = logElements {
                add(requestMethodKey, request.requestLine.method.toString())
                add(requestUriKey, request.requestLine.resourceUri.toString())
                add(responseCodeKey, message.statusLine.statusCode)
                add(responseDurationKey, requestDuration.toMillis())
            }
            logResponse(request, response, elements)
        }
        responder.respond(message)
    }

    private fun logResponse(
        request: HTTPRequestMessage,
        response: HTTPRouteResponse,
        elements: () -> PelletLogElements
    ) {
        val dateTime = commonDateFormat.format(Instant.now().atZone(UTC))
        val (method, uri, version) = request.requestLine
        val responseSize = response.entity.sizeBytes
        logger.info(elements) { "${client.remoteHostString} - - [$dateTime] \"$method $uri $version\" ${response.statusCode} $responseSize" }
    }

    private fun handleConnectionHeader(connectionHeader: HTTPHeader?) {
        if (connectionHeader == null) {
            // keep alive by default
            return
        }

        if (connectionHeader.rawValue.equals(HTTPHeaderConstants.keepAlive, ignoreCase = true)) {
            return
        }

        if (connectionHeader.rawValue.equals(HTTPHeaderConstants.close, ignoreCase = true)) {
            client.close(CloseReason.ServerInitiated)
        }
    }

    private fun mapRouteResponseToMessage(
        routeResult: HTTPRouteResponse
    ): HTTPResponseMessage {
        val effectiveStatusCode = when (routeResult.statusCode) {
            0 -> 200
            else -> routeResult.statusCode
        }
        return HTTPResponseMessage(
            statusLine = HTTPStatusLine(
                version = "HTTP/1.1",
                statusCode = effectiveStatusCode,
                reasonPhrase = mapCodeToReasonPhrase(effectiveStatusCode)
            ),
            headers = routeResult.headers,
            entity = routeResult.entity
        )
    }

    private fun mapCodeToReasonPhrase(code: Int) = when (code) {
        200 -> "OK"
        204 -> "No Content"
        404 -> "Not Found"
        500 -> "Internal Server Error"
        else -> "Unknown"
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy