Skip to content

Commit 13a0b47

Browse files
committed
Update docs, add helper
1 parent ec9bf6c commit 13a0b47

5 files changed

Lines changed: 80 additions & 9 deletions

File tree

README.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,25 +60,41 @@ Here is an example of how to use the library to create a simple "Hello, World!"
6060
```kotlin
6161
import com.google.gson.JsonObject
6262
import net.ccbluex.netty.http.HttpServer
63-
import net.ccbluex.netty.http.util.httpOk
63+
import net.ccbluex.netty.http.routing.Routing
64+
import net.ccbluex.netty.http.routing.RoutingContext
6465

6566
suspend fun main() {
6667
val server = HttpServer()
6768

6869
server.routing {
69-
get("/hello") {
70-
httpOk(JsonObject().apply {
71-
addProperty("message", "Hello, World!")
72-
})
73-
}
70+
helloRoutes()
7471
}
7572

7673
server.start(8080) // Start the server on port 8080
7774
}
75+
76+
fun Routing.helloRoutes() {
77+
get("/hello") { getHello() }
78+
}
79+
80+
private suspend fun RoutingContext.getHello() {
81+
respond(JsonObject().apply {
82+
addProperty("message", "Hello, World!")
83+
})
84+
}
7885
```
7986

8087
In this example, the server listens on port `8080` and responds with a JSON message `"Hello, World!"` when accessing the `/hello` endpoint.
8188

89+
### Responding and aborting
90+
91+
`RoutingContext` handlers should either:
92+
93+
1. Call `respond(...)`, `respondNoContent()`, `respondFile(...)`, or `respondFileStream(...)`, or
94+
2. Abort with `badRequest(...)`, `forbidden(...)`, `unauthorized(...)`, `notFound(...)`, `serviceUnavailable(...)`, or `internalServerError(...)`.
95+
96+
The abort helpers throw an internal `ResponseException` to stop the handler immediately, so they should not be wrapped in a broad `catch (Exception)` unless you rethrow that control-flow exception.
97+
8298
### Examples
8399

84100
You can find additional examples in the `/examples` folder of the repository. These include:
@@ -119,4 +135,3 @@ Netty HttpServer is developed and maintained by CCBlueX. It was originally part
119135
---
120136

121137
Feel free to explore the examples provided and adapt them to your specific needs. Happy coding!
122-

src/main/kotlin/net/ccbluex/netty/http/application/ApplicationCall.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package net.ccbluex.netty.http.application
22

33
import com.google.gson.Gson
44
import com.google.gson.JsonElement
5+
import io.netty.buffer.PooledByteBufAllocator
56
import io.netty.handler.codec.http.FullHttpResponse
67
import io.netty.handler.codec.http.HttpHeaders
78
import io.netty.handler.codec.http.HttpMethod
@@ -15,9 +16,14 @@ import net.ccbluex.netty.http.util.httpInternalServerError
1516
import net.ccbluex.netty.http.util.httpNoContent
1617
import net.ccbluex.netty.http.util.httpNotFound
1718
import net.ccbluex.netty.http.util.httpResponse
19+
import net.ccbluex.netty.http.util.httpServiceUnavailable
1820
import net.ccbluex.netty.http.util.httpUnauthorized
21+
import net.ccbluex.netty.http.util.inputStream
22+
import net.ccbluex.netty.http.util.outputStream
23+
import net.ccbluex.netty.http.util.tika
1924
import java.io.File
2025
import java.io.InputStream
26+
import java.io.OutputStream
2127

2228
open class ApplicationCall(
2329
val uri: String,
@@ -75,6 +81,23 @@ open class ApplicationCall(
7581
respond(httpFileStream(stream, contentType, contentLength))
7682
}
7783

84+
@JvmOverloads
85+
fun respondOutputStream(
86+
contentType: String? = null,
87+
status: HttpResponseStatus = HttpResponseStatus.OK,
88+
contentLength: Int = 256,
89+
producer: OutputStream.() -> Unit,
90+
) {
91+
val allocator = PooledByteBufAllocator.DEFAULT
92+
val buf = allocator.buffer(contentLength)
93+
94+
buf.outputStream().use {
95+
producer(it)
96+
}
97+
98+
respond(httpResponse(status, contentType ?: tika.detect(buf.duplicate().inputStream()), buf))
99+
}
100+
78101
fun takeResponse(): FullHttpResponse {
79102
return response ?: throw IllegalStateException("Route handler completed without responding for $method $path")
80103
}
@@ -87,6 +110,8 @@ open class ApplicationCall(
87110

88111
fun notFound(path: String, reason: String): Nothing = abort(httpNotFound(path, reason))
89112

113+
fun serviceUnavailable(reason: String): Nothing = abort(httpServiceUnavailable(reason))
114+
90115
fun internalServerError(reason: String): Nothing = abort(httpInternalServerError(reason))
91116

92117
private fun abort(response: FullHttpResponse): Nothing = throw ResponseException(response)

src/main/kotlin/net/ccbluex/netty/http/routing/RoutingContext.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import net.ccbluex.netty.http.application.ApplicationCall
99
import net.ccbluex.netty.http.rest.Node
1010
import java.io.File
1111
import java.io.InputStream
12-
import java.lang.reflect.Type
1312

1413
typealias RoutingHandler = suspend RoutingContext.() -> Unit
1514

@@ -56,5 +55,6 @@ class RoutingContext(
5655
fun forbidden(reason: String): Nothing = call.forbidden(reason)
5756
fun unauthorized(reason: String): Nothing = call.unauthorized(reason)
5857
fun notFound(path: String, reason: String): Nothing = call.notFound(path, reason)
58+
fun serviceUnavailable(reason: String): Nothing = call.serviceUnavailable(reason)
5959
fun internalServerError(reason: String): Nothing = call.internalServerError(reason)
6060
}

src/main/kotlin/net/ccbluex/netty/http/util/HttpResponse.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ fun httpBadRequest(reason: String): FullHttpResponse {
169169
return httpResponse(HttpResponseStatus.BAD_REQUEST, ResponseBody(reason))
170170
}
171171

172-
private val tika = Tika()
172+
internal val tika = Tika()
173173

174174
/**
175175
* Creates an HTTP response for the given file.

src/test/kotlin/HttpServerTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ class HttpServerTest {
122122
get("/v/:name") { param() }
123123
get("/r/:value1/:value2") { params() }
124124
get("/o/:value1/in/:value2") { params() }
125+
route("/errors") {
126+
get("/bad-request") {
127+
badRequest("Bad request")
128+
}
129+
get("/service-unavailable") {
130+
serviceUnavailable("Service unavailable")
131+
}
132+
}
125133
}
126134

127135
private fun Routing.nestedRoutes() {
@@ -355,6 +363,29 @@ class HttpServerTest {
355363
assertEquals(404, response.code, "Expected status code 404")
356364
}
357365

366+
@Test
367+
fun testBadRequestAbortEndpoint() {
368+
val response = makeRequest("/errors/bad-request")
369+
assertEquals(400, response.code, "Expected status code 400")
370+
371+
val responseBody = response.body?.string()
372+
assertNotNull(responseBody, "Response body should not be null")
373+
assertTrue(responseBody.contains("\"reason\":\"Bad request\""), "Response should contain the abort reason")
374+
}
375+
376+
@Test
377+
fun testServiceUnavailableAbortEndpoint() {
378+
val response = makeRequest("/errors/service-unavailable")
379+
assertEquals(503, response.code, "Expected status code 503")
380+
381+
val responseBody = response.body?.string()
382+
assertNotNull(responseBody, "Response body should not be null")
383+
assertTrue(
384+
responseBody.contains("\"reason\":\"Service unavailable\""),
385+
"Response should contain the abort reason"
386+
)
387+
}
388+
358389
@Test
359390
fun testFileEndpoint() {
360391
testFileEndpoint("/abc")

0 commit comments

Comments
 (0)