From ff3e2dc4b1cea24ce692c8f289f1406f166839a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 20 May 2025 12:15:49 +0800
Subject: [PATCH 1/9] refactor(rest): improve response creation
---
.../netty/http/util/ByteBufExtensions.kt | 29 ++++
.../ccbluex/netty/http/util/HttpResponse.kt | 146 +++++++++++-------
.../ccbluex/netty/http/util/Serializations.kt | 51 ++++++
3 files changed, 170 insertions(+), 56 deletions(-)
create mode 100644 src/main/kotlin/net/ccbluex/netty/http/util/ByteBufExtensions.kt
create mode 100644 src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/ByteBufExtensions.kt b/src/main/kotlin/net/ccbluex/netty/http/util/ByteBufExtensions.kt
new file mode 100644
index 0000000..271de03
--- /dev/null
+++ b/src/main/kotlin/net/ccbluex/netty/http/util/ByteBufExtensions.kt
@@ -0,0 +1,29 @@
+/*
+ * This file is part of Netty-Rest (https://github.com/CCBlueX/netty-rest)
+ *
+ * Copyright (c) 2024 CCBlueX
+ *
+ * LiquidBounce is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Netty-Rest is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Netty-Rest. If not, see .
+ *
+ */
+@file:Suppress("NOTHING_TO_INLINE")
+package net.ccbluex.netty.http.util
+
+import io.netty.buffer.ByteBuf
+import io.netty.buffer.ByteBufInputStream
+import io.netty.buffer.ByteBufOutputStream
+
+inline fun ByteBuf.inputStream() = ByteBufInputStream(this)
+
+inline fun ByteBuf.outputStream() = ByteBufOutputStream(this)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
index eec1244..3bc6e64 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
@@ -19,29 +19,32 @@
*/
package net.ccbluex.netty.http.util
-import com.google.gson.Gson
import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import io.netty.buffer.Unpooled
+import io.netty.buffer.ByteBuf
+import io.netty.buffer.PooledByteBufAllocator
import io.netty.handler.codec.http.*
import org.apache.tika.Tika
import java.io.File
import java.io.InputStream
+import java.lang.reflect.Type
/**
* Creates an HTTP response with the given status, content type, and content.
*
* @param status The HTTP response status.
* @param contentType The content type of the response. Defaults to "text/plain".
- * @param content The content of the response.
+ * @param content The content of the response, in [io.netty.buffer.ByteBuf].
* @return A FullHttpResponse object.
*/
-fun httpResponse(status: HttpResponseStatus, contentType: String = "text/plain",
- content: String): FullHttpResponse {
+fun httpResponse(
+ status: HttpResponseStatus,
+ contentType: String = "text/plain",
+ content: ByteBuf
+): FullHttpResponse {
val response = DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
status,
- Unpooled.wrappedBuffer(content.toByteArray())
+ content
)
val httpHeaders = response.headers()
@@ -51,6 +54,37 @@ fun httpResponse(status: HttpResponseStatus, contentType: String = "text/plain",
return response
}
+/**
+ * Creates an HTTP response with the given status, content type, and content.
+ *
+ * @param status The HTTP response status.
+ * @param contentType The content type of the response. Defaults to "text/plain".
+ * @param content The content of the response, in [String].
+ * @return A FullHttpResponse object.
+ */
+fun httpResponse(
+ status: HttpResponseStatus,
+ contentType: String = "text/plain",
+ content: String
+): FullHttpResponse {
+ val buf = PooledByteBufAllocator.DEFAULT.buffer(content.length * 3)
+ buf.writeCharSequence(content, Charsets.UTF_8)
+ return httpResponse(status, contentType, buf)
+}
+
+/**
+ * Creates an HTTP response with the given status and JSON content.
+ *
+ * @param status The HTTP response status.
+ * @param json The JSON content of the response.
+ * @return A FullHttpResponse object.
+ */
+fun httpResponse(status: HttpResponseStatus, json: JsonElement) = httpResponse(
+ status,
+ "application/json",
+ PooledByteBufAllocator.DEFAULT.writeJson(json)
+)
+
/**
* Creates an HTTP response with the given status and JSON content.
*
@@ -58,8 +92,11 @@ fun httpResponse(status: HttpResponseStatus, contentType: String = "text/plain",
* @param json The JSON content of the response.
* @return A FullHttpResponse object.
*/
-fun httpResponse(status: HttpResponseStatus, json: JsonElement)
- = httpResponse(status, "application/json", Gson().toJson(json))
+fun httpResponse(status: HttpResponseStatus, json: T, type: Type) = httpResponse(
+ status,
+ "application/json",
+ PooledByteBufAllocator.DEFAULT.writeJson(json, type)
+)
/**
* Creates an HTTP 200 OK response with the given JSON content.
@@ -67,8 +104,7 @@ fun httpResponse(status: HttpResponseStatus, json: JsonElement)
* @param jsonElement The JSON content of the response.
* @return A FullHttpResponse object.
*/
-fun httpOk(jsonElement: JsonElement)
- = httpResponse(HttpResponseStatus.OK, jsonElement)
+fun httpOk(jsonElement: JsonElement) = httpResponse(HttpResponseStatus.OK, jsonElement)
/**
* Creates an HTTP 404 Not Found response with the given path and reason.
@@ -78,10 +114,8 @@ fun httpOk(jsonElement: JsonElement)
* @return A FullHttpResponse object.
*/
fun httpNotFound(path: String, reason: String): FullHttpResponse {
- val jsonObject = JsonObject()
- jsonObject.addProperty("path", path)
- jsonObject.addProperty("reason", reason)
- return httpResponse(HttpResponseStatus.NOT_FOUND, jsonObject)
+ data class ResponseBody(val path: String, val reason: String)
+ return httpResponse(HttpResponseStatus.NOT_FOUND, ResponseBody(path, reason), ResponseBody::class.java)
}
/**
@@ -91,9 +125,8 @@ fun httpNotFound(path: String, reason: String): FullHttpResponse {
* @return A FullHttpResponse object.
*/
fun httpInternalServerError(exception: String): FullHttpResponse {
- val jsonObject = JsonObject()
- jsonObject.addProperty("reason", exception)
- return httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, jsonObject)
+ data class ResponseBody(val reason: String)
+ return httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, ResponseBody(exception), ResponseBody::class.java)
}
/**
@@ -103,9 +136,8 @@ fun httpInternalServerError(exception: String): FullHttpResponse {
* @return A FullHttpResponse object.
*/
fun httpForbidden(reason: String): FullHttpResponse {
- val jsonObject = JsonObject()
- jsonObject.addProperty("reason", reason)
- return httpResponse(HttpResponseStatus.FORBIDDEN, jsonObject)
+ data class ResponseBody(val reason: String)
+ return httpResponse(HttpResponseStatus.FORBIDDEN, ResponseBody(reason), ResponseBody::class.java)
}
/**
@@ -115,9 +147,8 @@ fun httpForbidden(reason: String): FullHttpResponse {
* @return A FullHttpResponse object.
*/
fun httpBadRequest(reason: String): FullHttpResponse {
- val jsonObject = JsonObject()
- jsonObject.addProperty("reason", reason)
- return httpResponse(HttpResponseStatus.BAD_REQUEST, jsonObject)
+ data class ResponseBody(val reason: String)
+ return httpResponse(HttpResponseStatus.BAD_REQUEST, ResponseBody(reason), ResponseBody::class.java)
}
private val tika = Tika()
@@ -129,16 +160,21 @@ private val tika = Tika()
* @return A FullHttpResponse object.
*/
fun httpFile(file: File): FullHttpResponse {
- val response = DefaultFullHttpResponse(
- HttpVersion.HTTP_1_1,
- HttpResponseStatus.OK,
- Unpooled.wrappedBuffer(file.readBytes())
- )
+ val buf = file.inputStream().channel.use { channel ->
+ val size = channel.size().toInt()
+ val buf = PooledByteBufAllocator.DEFAULT.buffer(size)
+ while (buf.writableBytes() > 0) {
+ val written = buf.writeBytes(channel, buf.writableBytes())
+ if (written <= 0) break
+ }
+ buf
+ }
- val httpHeaders = response.headers()
- httpHeaders[HttpHeaderNames.CONTENT_TYPE] = tika.detect(file)
- httpHeaders[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes()
- return response
+ return httpResponse(
+ status = HttpResponseStatus.OK,
+ contentType = tika.detect(file),
+ content = buf
+ )
}
/**
@@ -148,19 +184,21 @@ fun httpFile(file: File): FullHttpResponse {
* @return A FullHttpResponse object.
*/
fun httpFileStream(stream: InputStream): FullHttpResponse {
- val bytes = stream.readBytes()
+ val allocator = PooledByteBufAllocator.DEFAULT
+ val buf = allocator.buffer()
- val response = DefaultFullHttpResponse(
- HttpVersion.HTTP_1_1,
- HttpResponseStatus.OK,
- Unpooled.wrappedBuffer(bytes)
- )
-
- val httpHeaders = response.headers()
- httpHeaders[HttpHeaderNames.CONTENT_TYPE] = tika.detect(bytes)
- httpHeaders[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes()
+ val tmp = ByteArray(8192)
+ while (true) {
+ val read = stream.read(tmp)
+ if (read == -1) break
+ buf.writeBytes(tmp, 0, read)
+ }
- return response
+ return httpResponse(
+ status = HttpResponseStatus.OK,
+ contentType = tika.detect(buf.duplicate().inputStream()),
+ content = buf
+ )
}
/**
@@ -186,9 +224,8 @@ fun httpNoContent(): FullHttpResponse {
* @return A FullHttpResponse object.
*/
fun httpMethodNotAllowed(method: String): FullHttpResponse {
- val jsonObject = JsonObject()
- jsonObject.addProperty("method", method)
- return httpResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, jsonObject)
+ data class ResponseBody(val method: String)
+ return httpResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, ResponseBody(method), ResponseBody::class.java)
}
/**
@@ -198,9 +235,8 @@ fun httpMethodNotAllowed(method: String): FullHttpResponse {
* @return A FullHttpResponse object.
*/
fun httpUnauthorized(reason: String): FullHttpResponse {
- val jsonObject = JsonObject()
- jsonObject.addProperty("reason", reason)
- return httpResponse(HttpResponseStatus.UNAUTHORIZED, jsonObject)
+ data class ResponseBody(val reason: String)
+ return httpResponse(HttpResponseStatus.UNAUTHORIZED, ResponseBody(reason), ResponseBody::class.java)
}
/**
@@ -210,9 +246,8 @@ fun httpUnauthorized(reason: String): FullHttpResponse {
* @return A FullHttpResponse object.
*/
fun httpTooManyRequests(reason: String): FullHttpResponse {
- val jsonObject = JsonObject()
- jsonObject.addProperty("reason", reason)
- return httpResponse(HttpResponseStatus.TOO_MANY_REQUESTS, jsonObject)
+ data class ResponseBody(val reason: String)
+ return httpResponse(HttpResponseStatus.TOO_MANY_REQUESTS, ResponseBody(reason), ResponseBody::class.java)
}
/**
@@ -222,7 +257,6 @@ fun httpTooManyRequests(reason: String): FullHttpResponse {
* @return A FullHttpResponse object.
*/
fun httpServiceUnavailable(reason: String): FullHttpResponse {
- val jsonObject = JsonObject()
- jsonObject.addProperty("reason", reason)
- return httpResponse(HttpResponseStatus.SERVICE_UNAVAILABLE, jsonObject)
+ data class ResponseBody(val reason: String)
+ return httpResponse(HttpResponseStatus.SERVICE_UNAVAILABLE, ResponseBody(reason), ResponseBody::class.java)
}
diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
new file mode 100644
index 0000000..f323baf
--- /dev/null
+++ b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
@@ -0,0 +1,51 @@
+/*
+ * This file is part of Netty-Rest (https://github.com/CCBlueX/netty-rest)
+ *
+ * Copyright (c) 2024 CCBlueX
+ *
+ * LiquidBounce is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Netty-Rest is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Netty-Rest. If not, see .
+ *
+ */
+package net.ccbluex.netty.http.util
+
+import com.google.gson.Gson
+import com.google.gson.JsonElement
+import io.netty.buffer.ByteBuf
+import io.netty.buffer.ByteBufAllocator
+import java.lang.reflect.Type
+
+private val gson = Gson()
+
+fun ByteBufAllocator.writeJson(
+ json: JsonElement,
+): ByteBuf {
+ val buf = buffer(256, Int.MAX_VALUE)
+ gson.newJsonWriter(buf.outputStream().writer(Charsets.UTF_8)).use { writer ->
+ gson.toJson(json, writer)
+ writer.flush()
+ }
+ return buf
+}
+
+fun ByteBufAllocator.writeJson(
+ obj: T,
+ type: Type
+): ByteBuf {
+ val buf = buffer(256, Int.MAX_VALUE)
+ gson.newJsonWriter(buf.outputStream().writer(Charsets.UTF_8)).use { writer ->
+ gson.toJson(obj, type, writer)
+ writer.flush()
+ }
+ return buf
+}
From 7630c5fa6710f982a33979d38d9fbd4d633e9cf6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 20 May 2025 13:08:09 +0800
Subject: [PATCH 2/9] shared GSON
---
.../kotlin/net/ccbluex/netty/http/model/RequestObject.kt | 9 +++++++--
.../kotlin/net/ccbluex/netty/http/util/Serializations.kt | 2 +-
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt b/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt
index cd73ad0..77c935f 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt
@@ -19,8 +19,8 @@
*/
package net.ccbluex.netty.http.model
-import com.google.gson.Gson
import io.netty.handler.codec.http.HttpMethod
+import net.ccbluex.netty.http.util.gson
/**
* Represents an HTTP request object.
@@ -51,7 +51,12 @@ data class RequestObject(
* @return The JSON object of the specified type.
*/
inline fun asJson(): T {
- return Gson().fromJson(body, T::class.java)
+ return GSON_INSTANCE.fromJson(body, T::class.java)
+ }
+
+ companion object {
+ @JvmField
+ val GSON_INSTANCE = gson
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
index f323baf..4a87c23 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
@@ -25,7 +25,7 @@ import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import java.lang.reflect.Type
-private val gson = Gson()
+internal val gson = Gson()
fun ByteBufAllocator.writeJson(
json: JsonElement,
From f6d446f60d112939ef1c6131fa1450e7de2b165a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 20 May 2025 13:08:31 +0800
Subject: [PATCH 3/9] other stuffs
---
.../net/ccbluex/netty/http/HttpConductor.kt | 2 +-
.../ccbluex/netty/http/HttpServerHandler.kt | 2 +-
.../netty/http/model/RequestContext.kt | 35 ++++++++-----------
3 files changed, 17 insertions(+), 22 deletions(-)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
index f4a968f..11baa89 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
@@ -52,7 +52,7 @@ internal class HttpConductor(private val server: HttpServer) {
val response = DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
- Unpooled.wrappedBuffer(ByteArray(0))
+ Unpooled.EMPTY_BUFFER
)
val httpHeaders = response.headers()
diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt
index 031659f..546e4f0 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt
@@ -86,7 +86,7 @@ internal class HttpServerHandler(private val server: HttpServer) : ChannelInboun
} else {
val requestContext = RequestContext(
msg.method(),
- URLDecoder.decode(msg.uri(), "UTF-8"),
+ URLDecoder.decode(msg.uri(), Charsets.UTF_8),
msg.headers().associate { it.key to it.value },
)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt b/src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt
index cf5bb6e..608902d 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt
@@ -20,12 +20,10 @@
package net.ccbluex.netty.http.model
import io.netty.handler.codec.http.HttpMethod
-import java.util.*
-import java.util.stream.Collectors
data class RequestContext(var httpMethod: HttpMethod, var uri: String, var headers: Map) {
val contentBuffer = StringBuilder()
- val path = if (uri.contains("?")) uri.substring(0, uri.indexOf('?')) else uri
+ val path = uri.substringBefore('?', uri)
val params = getUriParams(uri)
}
@@ -33,24 +31,21 @@ data class RequestContext(var httpMethod: HttpMethod, var uri: String, var heade
* The received uri should be like: '...?param1=value¶m2=value'
*/
private fun getUriParams(uri: String): Map {
- if (uri.contains("?")) {
- val paramsString = uri.substring(uri.indexOf('?') + 1)
+ val queryString = uri.substringAfter('?', "")
- // in case of duplicated params, will be used la last value
- return Arrays.stream(
- if (paramsString.contains("&")) paramsString.split("&".toRegex()).dropLastWhile { it.isEmpty() }
- .toTypedArray() else arrayOf(paramsString))
- .map { value: String ->
- value.split("=".toRegex()).dropLastWhile { it.isEmpty() }
- .toTypedArray()
- }
- .collect(
- Collectors.toMap(
- { paramValue: Array -> paramValue[0] },
- { paramValue: Array -> paramValue[1] },
- { v1: String?, v2: String -> v2 })
- )
+ if (queryString.isEmpty()) {
+ return emptyMap()
}
- return emptyMap()
+ // in case of duplicated params, will be used the last value
+ return queryString.split('&')
+ .mapNotNull { param ->
+ val index = param.indexOf('=')
+ if (index == -1) null
+ else {
+ val key = param.substring(0, index)
+ val value = param.substring(index + 1)
+ if (key.isNotEmpty()) key to value else null
+ }
+ }.toMap()
}
From b649d26f8ec67768b696e1f7828b5d940ba90784 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 20 May 2025 17:21:59 +0800
Subject: [PATCH 4/9] small optimizations
---
.../net/ccbluex/netty/http/HttpConductor.kt | 19 ++++++-------------
.../ccbluex/netty/http/HttpServerHandler.kt | 5 ++---
.../ccbluex/netty/http/rest/RouteControl.kt | 8 ++++++--
3 files changed, 14 insertions(+), 18 deletions(-)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
index 11baa89..17e6568 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
@@ -27,6 +27,8 @@ import net.ccbluex.netty.http.util.httpBadRequest
import net.ccbluex.netty.http.util.httpInternalServerError
import net.ccbluex.netty.http.util.httpNotFound
import net.ccbluex.netty.http.model.RequestObject
+import net.ccbluex.netty.http.util.httpNoContent
+import net.ccbluex.netty.http.util.httpResponse
internal class HttpConductor(private val server: HttpServer) {
@@ -48,22 +50,13 @@ internal class HttpConductor(private val server: HttpServer) {
return@runCatching httpBadRequest("Incomplete request")
}
- if (method == HttpMethod.OPTIONS) {
- val response = DefaultFullHttpResponse(
- HttpVersion.HTTP_1_1,
- HttpResponseStatus.OK,
- Unpooled.EMPTY_BUFFER
- )
-
- val httpHeaders = response.headers()
- httpHeaders[HttpHeaderNames.CONTENT_TYPE] = "text/plain"
- httpHeaders[HttpHeaderNames.CONTENT_LENGTH] = response.content().readableBytes()
- return@runCatching response
- }
-
val (node, params, remaining) = server.routeController.processPath(context.path, method) ?:
return@runCatching httpNotFound(context.path, "Route not found")
+ if (method == HttpMethod.OPTIONS) {
+ return@runCatching httpNoContent()
+ }
+
logger.debug("Found destination {}", node)
val requestObject = RequestObject(
uri = context.uri,
diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt
index 546e4f0..b6e2f12 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt
@@ -95,13 +95,12 @@ internal class HttpServerHandler(private val server: HttpServer) : ChannelInboun
}
is HttpContent -> {
- if (localRequestContext.get() == null) {
+ val requestContext = localRequestContext.get() ?: run {
logger.warn("Received HttpContent without HttpRequest")
return
}
// Append content to the buffer
- val requestContext = localRequestContext.get()
requestContext
.contentBuffer
.append(msg.content().toString(Charsets.UTF_8))
@@ -109,7 +108,7 @@ internal class HttpServerHandler(private val server: HttpServer) : ChannelInboun
// If this is the last content, process the request
if (msg is LastHttpContent) {
localRequestContext.remove()
-
+
val httpConductor = HttpConductor(server)
val response = httpConductor.processRequestContext(requestContext)
val httpResponse = server.middlewares.fold(response) { acc, f -> f(requestContext, acc) }
diff --git a/src/main/kotlin/net/ccbluex/netty/http/rest/RouteControl.kt b/src/main/kotlin/net/ccbluex/netty/http/rest/RouteControl.kt
index 84b5bc5..54a752d 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/rest/RouteControl.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/rest/RouteControl.kt
@@ -55,7 +55,7 @@ class RouteController : Node("") {
*/
internal fun processPath(path: String, method: HttpMethod): Destination? {
val pathArray = path.asPathArray()
- .also { if (it.isEmpty()) throw IllegalArgumentException("Path cannot be empty") }
+ require(pathArray.isNotEmpty()) { "Path cannot be empty" }
return travelNode(this, pathArray, method, 0, mutableMapOf())
}
@@ -282,4 +282,8 @@ class FileServant(part: String, private val baseFolder: File) : Node(part) {
*
* @return An array of path parts.
*/
-private fun String.asPathArray() = split("/").drop(1).toTypedArray()
\ No newline at end of file
+private fun String.asPathArray(): Array {
+ val parts = split("/")
+ return if (parts.size <= 1) emptyArray()
+ else parts.subList(1, parts.size).toTypedArray()
+}
\ No newline at end of file
From f02c81e2d36dfb2aaa87fa30485bc8755300d01c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 20 May 2025 17:23:10 +0800
Subject: [PATCH 5/9] imports
---
src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
index 17e6568..69d1e90 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
@@ -19,7 +19,6 @@
*/
package net.ccbluex.netty.http
-import io.netty.buffer.Unpooled
import io.netty.handler.codec.http.*
import net.ccbluex.netty.http.HttpServer.Companion.logger
import net.ccbluex.netty.http.model.RequestContext
@@ -28,7 +27,6 @@ import net.ccbluex.netty.http.util.httpInternalServerError
import net.ccbluex.netty.http.util.httpNotFound
import net.ccbluex.netty.http.model.RequestObject
import net.ccbluex.netty.http.util.httpNoContent
-import net.ccbluex.netty.http.util.httpResponse
internal class HttpConductor(private val server: HttpServer) {
From 30f9603b2321751c853df4236a2fff01a56f3d9b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 20 May 2025 17:27:15 +0800
Subject: [PATCH 6/9] remove type param
---
.../ccbluex/netty/http/util/HttpResponse.kt | 21 +++++++++----------
1 file changed, 10 insertions(+), 11 deletions(-)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
index 3bc6e64..503bf30 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
@@ -26,7 +26,6 @@ import io.netty.handler.codec.http.*
import org.apache.tika.Tika
import java.io.File
import java.io.InputStream
-import java.lang.reflect.Type
/**
* Creates an HTTP response with the given status, content type, and content.
@@ -92,10 +91,10 @@ fun httpResponse(status: HttpResponseStatus, json: JsonElement) = httpResponse(
* @param json The JSON content of the response.
* @return A FullHttpResponse object.
*/
-fun httpResponse(status: HttpResponseStatus, json: T, type: Type) = httpResponse(
+fun httpResponse(status: HttpResponseStatus, json: T) = httpResponse(
status,
"application/json",
- PooledByteBufAllocator.DEFAULT.writeJson(json, type)
+ PooledByteBufAllocator.DEFAULT.writeJson(json, (json as Any).javaClass)
)
/**
@@ -115,7 +114,7 @@ fun httpOk(jsonElement: JsonElement) = httpResponse(HttpResponseStatus.OK, jsonE
*/
fun httpNotFound(path: String, reason: String): FullHttpResponse {
data class ResponseBody(val path: String, val reason: String)
- return httpResponse(HttpResponseStatus.NOT_FOUND, ResponseBody(path, reason), ResponseBody::class.java)
+ return httpResponse(HttpResponseStatus.NOT_FOUND, ResponseBody(path, reason))
}
/**
@@ -126,7 +125,7 @@ fun httpNotFound(path: String, reason: String): FullHttpResponse {
*/
fun httpInternalServerError(exception: String): FullHttpResponse {
data class ResponseBody(val reason: String)
- return httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, ResponseBody(exception), ResponseBody::class.java)
+ return httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, ResponseBody(exception))
}
/**
@@ -137,7 +136,7 @@ fun httpInternalServerError(exception: String): FullHttpResponse {
*/
fun httpForbidden(reason: String): FullHttpResponse {
data class ResponseBody(val reason: String)
- return httpResponse(HttpResponseStatus.FORBIDDEN, ResponseBody(reason), ResponseBody::class.java)
+ return httpResponse(HttpResponseStatus.FORBIDDEN, ResponseBody(reason))
}
/**
@@ -148,7 +147,7 @@ fun httpForbidden(reason: String): FullHttpResponse {
*/
fun httpBadRequest(reason: String): FullHttpResponse {
data class ResponseBody(val reason: String)
- return httpResponse(HttpResponseStatus.BAD_REQUEST, ResponseBody(reason), ResponseBody::class.java)
+ return httpResponse(HttpResponseStatus.BAD_REQUEST, ResponseBody(reason))
}
private val tika = Tika()
@@ -225,7 +224,7 @@ fun httpNoContent(): FullHttpResponse {
*/
fun httpMethodNotAllowed(method: String): FullHttpResponse {
data class ResponseBody(val method: String)
- return httpResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, ResponseBody(method), ResponseBody::class.java)
+ return httpResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, ResponseBody(method))
}
/**
@@ -236,7 +235,7 @@ fun httpMethodNotAllowed(method: String): FullHttpResponse {
*/
fun httpUnauthorized(reason: String): FullHttpResponse {
data class ResponseBody(val reason: String)
- return httpResponse(HttpResponseStatus.UNAUTHORIZED, ResponseBody(reason), ResponseBody::class.java)
+ return httpResponse(HttpResponseStatus.UNAUTHORIZED, ResponseBody(reason))
}
/**
@@ -247,7 +246,7 @@ fun httpUnauthorized(reason: String): FullHttpResponse {
*/
fun httpTooManyRequests(reason: String): FullHttpResponse {
data class ResponseBody(val reason: String)
- return httpResponse(HttpResponseStatus.TOO_MANY_REQUESTS, ResponseBody(reason), ResponseBody::class.java)
+ return httpResponse(HttpResponseStatus.TOO_MANY_REQUESTS, ResponseBody(reason))
}
/**
@@ -258,5 +257,5 @@ fun httpTooManyRequests(reason: String): FullHttpResponse {
*/
fun httpServiceUnavailable(reason: String): FullHttpResponse {
data class ResponseBody(val reason: String)
- return httpResponse(HttpResponseStatus.SERVICE_UNAVAILABLE, ResponseBody(reason), ResponseBody::class.java)
+ return httpResponse(HttpResponseStatus.SERVICE_UNAVAILABLE, ResponseBody(reason))
}
From c0b708555d6036556cc3b74c012933f4552c449a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 20 May 2025 17:31:54 +0800
Subject: [PATCH 7/9] comments
---
.../net/ccbluex/netty/http/util/HttpResponse.kt | 4 ++--
.../net/ccbluex/netty/http/util/Serializations.kt | 11 +++++++++--
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
index 503bf30..5b6de70 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
@@ -91,10 +91,10 @@ fun httpResponse(status: HttpResponseStatus, json: JsonElement) = httpResponse(
* @param json The JSON content of the response.
* @return A FullHttpResponse object.
*/
-fun httpResponse(status: HttpResponseStatus, json: T) = httpResponse(
+fun httpResponse(status: HttpResponseStatus, json: T) = httpResponse(
status,
"application/json",
- PooledByteBufAllocator.DEFAULT.writeJson(json, (json as Any).javaClass)
+ PooledByteBufAllocator.DEFAULT.writeJson(json, json.javaClass)
)
/**
diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
index 4a87c23..4ff8280 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/util/Serializations.kt
@@ -27,6 +27,9 @@ import java.lang.reflect.Type
internal val gson = Gson()
+/**
+ * Serialize [json] into [ByteBuf] with given [ByteBufAllocator].
+ */
fun ByteBufAllocator.writeJson(
json: JsonElement,
): ByteBuf {
@@ -38,9 +41,13 @@ fun ByteBufAllocator.writeJson(
return buf
}
-fun ByteBufAllocator.writeJson(
+/**
+ * Serialize [obj] as [type] into [ByteBuf] with given [ByteBufAllocator].
+ */
+@JvmOverloads
+fun ByteBufAllocator.writeJson(
obj: T,
- type: Type
+ type: Type = obj.javaClass,
): ByteBuf {
val buf = buffer(256, Int.MAX_VALUE)
gson.newJsonWriter(buf.outputStream().writer(Charsets.UTF_8)).use { writer ->
From 525099e5b59c018b6c64ad62c0dad807f831fe78 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 20 May 2025 21:34:10 +0800
Subject: [PATCH 8/9] improve file stream response
---
.../ccbluex/netty/http/util/HttpResponse.kt | 52 ++++++++++---------
1 file changed, 27 insertions(+), 25 deletions(-)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
index 5b6de70..020e87e 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
@@ -25,6 +25,7 @@ import io.netty.buffer.PooledByteBufAllocator
import io.netty.handler.codec.http.*
import org.apache.tika.Tika
import java.io.File
+import java.io.IOException
import java.io.InputStream
/**
@@ -159,20 +160,9 @@ private val tika = Tika()
* @return A FullHttpResponse object.
*/
fun httpFile(file: File): FullHttpResponse {
- val buf = file.inputStream().channel.use { channel ->
- val size = channel.size().toInt()
- val buf = PooledByteBufAllocator.DEFAULT.buffer(size)
- while (buf.writableBytes() > 0) {
- val written = buf.writeBytes(channel, buf.writableBytes())
- if (written <= 0) break
- }
- buf
- }
-
- return httpResponse(
- status = HttpResponseStatus.OK,
+ return httpFileStream(
+ file.inputStream(),
contentType = tika.detect(file),
- content = buf
)
}
@@ -180,24 +170,36 @@ fun httpFile(file: File): FullHttpResponse {
* Creates an HTTP response for the given input stream.
*
* @param stream The input stream to be included in the response.
+ * It will be closed after reading.
+ * @param contentType The content type of [stream].
+ * Defaults to `null`, which means to auto-detect by [Tika].
* @return A FullHttpResponse object.
*/
-fun httpFileStream(stream: InputStream): FullHttpResponse {
+@JvmOverloads
+fun httpFileStream(
+ stream: InputStream,
+ contentType: String? = null
+): FullHttpResponse {
val allocator = PooledByteBufAllocator.DEFAULT
val buf = allocator.buffer()
- val tmp = ByteArray(8192)
- while (true) {
- val read = stream.read(tmp)
- if (read == -1) break
- buf.writeBytes(tmp, 0, read)
- }
+ try {
+ stream.use {
+ while (true) {
+ val read = buf.writeBytes(stream, 8192)
+ if (read == -1) break
+ }
+ }
- return httpResponse(
- status = HttpResponseStatus.OK,
- contentType = tika.detect(buf.duplicate().inputStream()),
- content = buf
- )
+ return httpResponse(
+ status = HttpResponseStatus.OK,
+ contentType = contentType ?: tika.detect(buf.duplicate().inputStream()),
+ content = buf
+ )
+ } catch (e: IOException) {
+ buf.release()
+ return httpInternalServerError(e.stackTraceToString())
+ }
}
/**
From 69a00cf46661345f5eb8194c9cec5367ede6f918 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 20 May 2025 21:40:35 +0800
Subject: [PATCH 9/9] add checks
---
.../net/ccbluex/netty/http/util/HttpResponse.kt | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
index 020e87e..7d52945 100644
--- a/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
+++ b/src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt
@@ -160,28 +160,33 @@ private val tika = Tika()
* @return A FullHttpResponse object.
*/
fun httpFile(file: File): FullHttpResponse {
+ require(file.length() <= Int.MAX_VALUE) { "File is too big" }
+
return httpFileStream(
file.inputStream(),
contentType = tika.detect(file),
+ contentLength = file.length().toInt()
)
}
/**
* Creates an HTTP response for the given input stream.
*
- * @param stream The input stream to be included in the response.
- * It will be closed after reading.
- * @param contentType The content type of [stream].
- * Defaults to `null`, which means to auto-detect by [Tika].
+ * @param stream The input stream to be included in the response. It will be closed after reading.
+ * @param contentType The content type of [stream]. Defaults to `null`, which means to auto-detect by [Tika].
+ * @param contentLength The predicated content length of [stream]. Defaults to `256`.
* @return A FullHttpResponse object.
*/
@JvmOverloads
fun httpFileStream(
stream: InputStream,
- contentType: String? = null
+ contentType: String? = null,
+ contentLength: Int = 256,
): FullHttpResponse {
+ require(contentLength > 0) { "content length must be positive" }
+
val allocator = PooledByteBufAllocator.DEFAULT
- val buf = allocator.buffer()
+ val buf = allocator.buffer(contentLength)
try {
stream.use {