Method Definitions
All API frameworks share a common base provided by the Reflection and Conversion modules which
define some of the capabilities that can be used when defining an API method
signature.
Injection
This section describes how parameters of your functions are initialized and which kind of types can be used.
Primitives
By default, a parameter type can be used within a method definition if it is either String,
Boolean, an enum, a number (such as Int, Long, Double or BigDecimal), a UUID, or one of
the date and time types Instant, OffsetDateTime, LocalDateTime, LocalDate, LocalTime and
kotlin.time.Duration.
Webservices
@ResourceMethod
fun length(text: String): Int = text.lengthFunctional
Inline.create()
.get { text: String -> text.length }Controllers
fun index(text: String): Int = text.lengthParameters can be declared nullable (e.g. Int?) and will be initialized with null if not present.
A parameter with a default value (e.g. limit: Int = 50) keeps its default value if not present or empty.
Otherwise, parameters of a primitive type such as Int, Double or Boolean are initialized with
0 or false if not present - so declare a parameter nullable or give it a default value if clients
may omit it. Kotlin does not allow default values on lambda parameters, so functional handlers declare
such parameters in a function and pass a function reference.
By default, parameters are read from the request query (?text=abc).
If you would like to read the parameter directly from the request body, you can mark it with the @FromBody annotation.
As lambda parameters cannot be annotated either, functional handlers use a function reference here as well.
Webservices
@ResourceMethod(Method.Put)
fun length(@FromBody text: String): Int = text.lengthFunctional
fun length(@FromBody text: String): Int = text.length
Inline.create()
.put(::length)Controllers
@ControllerAction(Method.Put)
fun index(@FromBody text: String): Int = text.lengthTo read the parameter from the request path, use the appropriate mechanism provided by the application framework.
Webservices
@ResourceMethod(Method.Delete, ":id")
fun delete(id: Int) { /* ... */ }Functional
Inline.create()
.delete(":id") { id: Int -> /* ... */ }Controllers
@ControllerAction(Method.Delete)
fun delete(@FromPath id: Int) { /* ... */ }Some frameworks allow to further restrict path parameters using a regular expression with a named group, whose name (letters only) is the name of the parameter.
Webservices
data class Book(val ean: Long, val title: String)
@ResourceMethod(path = "(?<ean>[0-9]{12,13})")
fun getBook(ean: Long): Book? = TODO("look up the book")Functional
Inline.create()
.get("books/(?<ean>[0-9]{12,13})") { ean: Long -> /* ... */ }Complex Types
When using a complex type in a parameter declaration, the value will be deserialized from the
request body. By default, handlers will accept content declared as XML, JSON, YAML or form encoded. If
the client does not declare the Content-Type, the server will try to treat the body as JSON. A body that
cannot be read is answered with 400 Bad Request, a Content-Type without a matching format with
415 Unsupported Media Type.
Webservices
data class Book(val id: Int, val title: String)
@ResourceMethod(Method.Post)
fun save(book: Book) { /* ... */ }Functional
data class Book(val id: Int, val title: String)
Inline.create()
.post { book: Book -> /* ... */ }Controllers
data class Book(val id: Int, val title: String)
@ControllerAction(Method.Post)
fun save(book: Book) { /* ... */ }HTML Forms
Form data can be used to populate both complex types and a generic list of key-value pairs
(BodyArguments, read with getEntry like the query). Browsers
will encode the content as application/x-www-form-urlencoded and the framework will
populate the arguments as needed. This allows such endpoints to be used both from
browsers and as a regular API.
Example form:
<form action="/save" method="post">
<label for="id">ID:</label>
<input type="number" id="id" name="id" required>
<br><br>
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<br><br>
<input type="submit" value="Submit">
</form>Can be read using the following definitions:
Webservices
data class Customer(val id: Int = 0, val name: String = "")
@ResourceMethod(Method.Post)
fun save(bodyArgs: BodyArguments) { /* ... */ }
// or
@ResourceMethod(Method.Post)
fun save(customer: Customer) { /* ... */ }Functional
data class Customer(val id: Int = 0, val name: String = "")
Inline.create()
.post("save") { bodyArgs: BodyArguments -> /* ... */ }
// or
Inline.create()
.post("save") { customer: Customer -> /* ... */ }Controllers
data class Customer(val id: Int = 0, val name: String = "")
@ControllerAction(Method.Post)
fun save(bodyArgs: BodyArguments) { /* ... */ }
// or
@ControllerAction(Method.Post)
fun save(customer: Customer) { /* ... */ }To read form data into a class, the framework creates an instance and then sets its properties by
name - so the class needs a constructor without parameters, or default values for all parameters of
its primary constructor, as Customer above. Fields that are no constructor parameter need to be
mutable (var) to be set.
Request Injection
To access information about the currently executed request you can add a parameter of
type Request to your method definition which will automatically be populated.
Webservices
@ResourceMethod
fun getUserAgent(request: Request): String? = request.header.headers.getEntry("User-Agent")Functional
Inline.create()
.get("user-agent") { request: Request -> request.header.headers.getEntry("User-Agent") }Controllers
fun userAgent(request: Request): String? = request.header.headers.getEntry("User-Agent")Injecting requests is required if you would like to generate custom responses.
If you frequently access the request in your endpoints to achieve a certain functionality, think about adding custom primitives, custom injectors or a custom concern.
Handler Injection
Similar to request injection you can also inject the Handler which is responsible for the
current request. This is typically not required but can be used to create and return
custom request handlers on the fly.
Streams
The request body can be injected as an InputStream, e.g. when implementing
file uploads. This stream represents the processed request payload, so it
will already be de-chunked (and decompressed, if decompression is
enabled). It reads the payload directly from the connection instead of buffering it, so
it is well suited for very large payloads - read it before your function returns.
Webservices
import org.genhttp.modules.reflection.*
import org.genhttp.modules.webservices.*
import java.io.InputStream
@ResourceMethod(Method.Put, "upload")
fun upload(file: InputStream) { /* ... */ }Functional
import org.genhttp.modules.functional.*
import java.io.InputStream
val api = Inline.create()
.put("upload") { file: InputStream -> /* ... */ }Controllers
import org.genhttp.modules.controllers.*
import org.genhttp.modules.reflection.*
import java.io.InputStream
@ControllerAction(Method.Put)
fun upload(file: InputStream) { /* ... */ }Custom Injection
To inject custom types besides the built-in capabilities, you can configure
a custom InjectionRegistry and use it with your API services. The registry accepts
ParameterInjector instances that define what types are supported and how
the value is determined for a given request.
import org.genhttp.api.content.Handler
import org.genhttp.api.infrastructure.Server
import org.genhttp.api.protocol.Request
import kotlin.reflect.KClass
interface ParameterInjector {
fun supports(server: Server, type: KClass<*>): Boolean
suspend fun getValue(handler: Handler, request: Request, targetType: KClass<*>): Any?
}supports is passed the server the service is running on and the type of the parameter, but not
the request - so its answer should only depend on the type and the server configuration. It is
evaluated when the service is prepared and again on every request, so keep it cheap.
The following injector will inspect the request headers for a correlation ID and create a new one if not present. It is added to the default injection registry, which is then passed to the service:
Webservices
import org.genhttp.api.content.Handler
import org.genhttp.api.infrastructure.Server
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.getEntry
import org.genhttp.modules.layouting.*
import org.genhttp.modules.reflection.*
import org.genhttp.modules.reflection.injectors.ParameterInjector
import org.genhttp.modules.webservices.*
import java.util.UUID
import kotlin.reflect.KClass
data class CorrelationId(val id: String)
class CorrelationInjector : ParameterInjector {
override fun supports(server: Server, type: KClass<*>): Boolean = type == CorrelationId::class
override suspend fun getValue(handler: Handler, request: Request, targetType: KClass<*>): Any {
val id = request.header.headers.getEntry("X-Correlation-ID")
return CorrelationId(id ?: UUID.randomUUID().toString())
}
}
val injection = Injection.default()
.add(CorrelationInjector())
val api = Layout.create()
.addService<MyService>("service", injection)
class MyService {
@ResourceMethod
fun getCorrelationId(correlation: CorrelationId): String = correlation.id
}Functional
import org.genhttp.api.content.Handler
import org.genhttp.api.infrastructure.Server
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.getEntry
import org.genhttp.modules.functional.*
import org.genhttp.modules.reflection.*
import org.genhttp.modules.reflection.injectors.ParameterInjector
import java.util.UUID
import kotlin.reflect.KClass
data class CorrelationId(val id: String)
class CorrelationInjector : ParameterInjector {
override fun supports(server: Server, type: KClass<*>): Boolean = type == CorrelationId::class
override suspend fun getValue(handler: Handler, request: Request, targetType: KClass<*>): Any {
val id = request.header.headers.getEntry("X-Correlation-ID")
return CorrelationId(id ?: UUID.randomUUID().toString())
}
}
val injection = Injection.default()
.add(CorrelationInjector())
val api = Inline.create()
.injectors(injection)
.get { correlation: CorrelationId -> correlation.id }Controllers
import org.genhttp.api.content.Handler
import org.genhttp.api.infrastructure.Server
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.getEntry
import org.genhttp.modules.controllers.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.reflection.*
import org.genhttp.modules.reflection.injectors.ParameterInjector
import java.util.UUID
import kotlin.reflect.KClass
data class CorrelationId(val id: String)
class CorrelationInjector : ParameterInjector {
override fun supports(server: Server, type: KClass<*>): Boolean = type == CorrelationId::class
override suspend fun getValue(handler: Handler, request: Request, targetType: KClass<*>): Any {
val id = request.header.headers.getEntry("X-Correlation-ID")
return CorrelationId(id ?: UUID.randomUUID().toString())
}
}
val injection = Injection.default()
.add(CorrelationInjector())
val api = Layout.create()
.addController<MyController>("controller", injection)
class MyController {
fun getCorrelationId(correlation: CorrelationId): String = correlation.id
}User Injection
To inject the authenticated user, you can add a typed injector to your injection registry.
import org.genhttp.modules.authentication.basic.BasicAuthenticationUser
import org.genhttp.modules.reflection.Injection
import org.genhttp.modules.reflection.injectors.UserInjector
val injection = Injection.default()
.add(UserInjector(BasicAuthenticationUser::class))Instead of injecting the user, you can also use a built-in interceptor to achieve role-based authorization.
Response Generation
This section describes the various mechanisms to generate a service response.
Primitives
By default, a type can be used as a return type within a method definition under the same rules
as parameter primitives: String, Boolean, enums, numbers, UUID or the date and
time types.
If the function returns null, the server will generate a HTTP 204 No Content.
Webservices
@ResourceMethod
fun length(text: String): Int = text.lengthFunctional
Inline.create()
.get { text: String -> text.length }Controllers
fun index(text: String): Int = text.lengthComplex Types
When returning a complex type, the value will be serialized and sent
to the client. The response format is negotiated with the client using the Accept request
header. By default, the server is capable of generating XML, JSON, YAML or form encoded responses. If
no format is specified by the client, the implementation will fall back to JSON.
If the function returns null, the server will generate a HTTP 204 No Content.
Webservices
data class Book(val id: Int, val title: String)
@ResourceMethod
fun doWork(): Book = Book(1, "The Hobbit")Functional
data class Book(val id: Int, val title: String)
Inline.create()
.get { Book(1, "The Hobbit") }Controllers
data class Book(val id: Int, val title: String)
fun index(): Book = Book(1, "The Hobbit")Custom Responses
When injecting the request into your function, you can directly generate a Response
or ResponseBuilder and return it to the client. This allows you to take full control
over the response generation but is less readable than the typed versions.
Webservices
@ResourceMethod
fun respond(request: Request): ResponseBuilder {
val content = Resource.fromString("Hello World")
.build()
return request.respond()
.header("X-My-Header", "my-value")
.content(content, ContentType.TextPlain)
}Functional
Inline.create()
.get { request: Request ->
val content = Resource.fromString("Hello World")
.build()
request.respond()
.header("X-My-Header", "my-value")
.content(content, ContentType.TextPlain)
}Controllers
fun respond(request: Request): ResponseBuilder {
val content = Resource.fromString("Hello World")
.build()
return request.respond()
.header("X-My-Header", "my-value")
.content(content, ContentType.TextPlain)
}For additional hints on how to use this mechanism to provide responses, see the response content section.
Results
Results are type-safe responses that can still be adjusted to modify the generated HTTP response. Therefore, results can be considered an advanced way to generate responses without the need to fully generate the response in the first place.
Webservices
@ResourceMethod
fun doWork(): Result<String> = Result("Hello World").header("X-My-Header", "my-value")Functional
Inline.create()
.get { Result("Hello World").header("X-My-Header", "my-value") }Controllers
fun index(): Result<String> = Result("Hello World").header("X-My-Header", "my-value")The Result<T> class lets you adjust the response around the payload via status(...),
connection(...), header(...) and cookie(...) - the payload itself is still serialized the
same way it would be if you had returned it directly. This does not only work
for data structures, but also for special types such as streams.
Result is org.genhttp.modules.reflection.Result, not the kotlin.Result of the standard library.
Kotlin prefers it as soon as it is imported, explicitly or with import org.genhttp.modules.reflection.*.
Handlers
Instead of generating a response you can also return a Handler or HandlerBuilder instance.
This allows you to provide a whole segment on your web application by re-using the
existing handlers or by implementing custom ones.
The following example will render a fully navigable directory listing view depending on the tenant ID passed to the function:
Webservices
@ResourceMethod(path = "files/:tenant")
fun files(tenant: Int): HandlerBuilder {
val tree = ResourceTree.fromDirectory("/data/tenants/$tenant")
return Listing.from(tree)
}Functional
Inline.create()
.get("files/:tenant") { tenant: Int -> Listing.from(ResourceTree.fromDirectory("/data/tenants/$tenant")) }Controllers
fun files(tenant: Int): HandlerBuilder {
val tree = ResourceTree.fromDirectory("/data/tenants/$tenant")
return Listing.from(tree)
}Binary Data
To return files or similar content, you can directly return an InputStream instance
from your function. The framework will automatically close the stream after it has been
sent. Returning streams is not thread-safe as streams are stateful, so you will need to
create a new instance for every request to be answered.
Besides streams, returning ByteArray and ByteBuffer is also supported.
Webservices
import org.genhttp.modules.webservices.*
import java.io.File
import java.io.InputStream
import java.nio.ByteBuffer
@ResourceMethod(path = "file")
fun getFile(): InputStream = File("...").inputStream()
@ResourceMethod(path = "bytes")
fun getBytes(): ByteArray = "Binary data".toByteArray()
@ResourceMethod(path = "memory")
fun getMemory(): ByteBuffer = ByteBuffer.wrap("Binary data".toByteArray())Functional
import org.genhttp.modules.functional.*
import java.io.File
import java.nio.ByteBuffer
val api = Inline.create()
.get("file") { File("...").inputStream() }
.get("bytes") { "Binary data".toByteArray() }
.get("memory") { ByteBuffer.wrap("Binary data".toByteArray()) }Controllers
import java.io.File
import java.io.InputStream
import java.nio.ByteBuffer
fun download(): InputStream = File("...").inputStream()
fun bytes(): ByteArray = "Binary data".toByteArray()
fun memory(): ByteBuffer = ByteBuffer.wrap("Binary data".toByteArray())Empty Responses
Functions returning Unit will automatically generate a HTTP 204 No Content
response. This is also the case when null is returned.
Webservices
@ResourceMethod
fun doWork() { }Functional
Inline.create()
.get { }Controllers
fun doWork() { }Asynchronous Execution
Service functions can be declared as suspend functions, and the lambdas of functional
handlers are suspend lambdas anyway, so they can call other suspending functions such as
delay or the API of a coroutine-based client. All the features described in this document
work for suspending functions as well. Blocking calls are fine too: requests are handled on
virtual threads, so a blocking call only parks a virtual thread.
Webservices
@ResourceMethod
suspend fun doWork(): Int {
delay(100)
return 42
}Functional
Inline.create()
.get {
delay(100)
42
}Controllers
suspend fun doWork(): Int {
delay(100)
return 42
}Registries
This section describes registries that can be used for both injection as well as response generation.
Custom Primitives
Primitives (such as UUID or Int) used in parameters or as a response type are automatically
handled using the built-in FormatterRegistry. You can add support for a custom type by
implementing a Formatter and adding it to a custom registry which is then used by your
services:
import org.genhttp.api.protocol.ByteString
import org.genhttp.api.protocol.ResponseContent
import kotlin.reflect.KClass
interface Formatter {
fun canHandle(type: KClass<*>): Boolean
fun read(value: ByteString, type: KClass<*>): Any?
fun write(value: Any, type: KClass<*>): String?
fun getContent(value: Any): ResponseContent
}read and write are used wherever the value only needs to be parsed or formatted as part
of a larger operation (e.g. a path or query parameter). getContent is used when the value is
the whole response body, so it needs to produce a full ResponseContent rather than just a string.
A typed variant of read is available as the extension function read<T>(value).
The following implementation will add support for a Point type with x and y coordinates
so it can be used in a service. The formatter is added to the default formatting registry,
which is then passed to the service:
Webservices
import org.genhttp.api.protocol.ByteString
import org.genhttp.api.protocol.ResponseContent
import org.genhttp.modules.conversion.*
import org.genhttp.modules.conversion.formatters.Formatter
import org.genhttp.modules.io.strings.StringContent
import org.genhttp.modules.layouting.*
import org.genhttp.modules.webservices.*
import kotlin.reflect.KClass
data class Point(val x: Int, val y: Int)
class PointFormatter : Formatter {
override fun canHandle(type: KClass<*>): Boolean = type == Point::class
override fun read(value: ByteString, type: KClass<*>): Any {
val (x, y) = value.toString().split('-')
return Point(x.toInt(), y.toInt())
}
override fun write(value: Any, type: KClass<*>): String {
val point = value as Point
return "${point.x}-${point.y}"
}
override fun getContent(value: Any): ResponseContent = StringContent(write(value, Point::class))
}
val registry = Formatting.default()
.add(PointFormatter())
val api = Layout.create()
.addService<MyService>("service", formatters = registry)
class MyService {
@ResourceMethod(path = "invert/:point")
fun invert(point: Point): Point = Point(point.y, point.x)
}Functional
import org.genhttp.api.protocol.ByteString
import org.genhttp.api.protocol.ResponseContent
import org.genhttp.modules.conversion.*
import org.genhttp.modules.conversion.formatters.Formatter
import org.genhttp.modules.functional.*
import org.genhttp.modules.io.strings.StringContent
import kotlin.reflect.KClass
data class Point(val x: Int, val y: Int)
class PointFormatter : Formatter {
override fun canHandle(type: KClass<*>): Boolean = type == Point::class
override fun read(value: ByteString, type: KClass<*>): Any {
val (x, y) = value.toString().split('-')
return Point(x.toInt(), y.toInt())
}
override fun write(value: Any, type: KClass<*>): String {
val point = value as Point
return "${point.x}-${point.y}"
}
override fun getContent(value: Any): ResponseContent = StringContent(write(value, Point::class))
}
val registry = Formatting.default()
.add(PointFormatter())
val api = Inline.create()
.formatters(registry)
.get("invert/:point") { point: Point -> Point(point.y, point.x) }Controllers
import org.genhttp.api.protocol.ByteString
import org.genhttp.api.protocol.ResponseContent
import org.genhttp.modules.controllers.*
import org.genhttp.modules.conversion.*
import org.genhttp.modules.conversion.formatters.Formatter
import org.genhttp.modules.io.strings.StringContent
import org.genhttp.modules.layouting.*
import kotlin.reflect.KClass
data class Point(val x: Int, val y: Int)
class PointFormatter : Formatter {
override fun canHandle(type: KClass<*>): Boolean = type == Point::class
override fun read(value: ByteString, type: KClass<*>): Any {
val (x, y) = value.toString().split('-')
return Point(x.toInt(), y.toInt())
}
override fun write(value: Any, type: KClass<*>): String {
val point = value as Point
return "${point.x}-${point.y}"
}
override fun getContent(value: Any): ResponseContent = StringContent(write(value, Point::class))
}
val registry = Formatting.default()
.add(PointFormatter())
val api = Layout.create()
.addController<MyController>("controller", formatters = registry)
class MyController {
fun invert(@FromPath point: Point): Point = Point(point.y, point.x)
}The serialization format is implemented by the formatter, so the API can be called
via /invert/8-10 and will return 10-8 as a text formatted response.
Serialization Formats
Serialization allows to read and write complex types, so they can be used in your method definitions. By default, services can consume and produce entities in XML, JSON, YAML or in form encoding, with a default fallback to JSON. JSON, XML and YAML are read and written with Jackson and its Kotlin module, so data classes work as they are.
When sending an entity to your service, the client should specify the Content-Type
of the body so the server can choose the correct deserializer to read the data with. The Accept
header sent by the client tells the server which serialization format is preferred by
the client when a response is generated. The server will tell the client which
serialization format was used to generate the body of the response by specifying the
Content-Type header again.

To add support for an additional format you can implement the SerializationFormat interface and
add your implementation to a registry which is then passed to your service, via .add(contentType, format).
For example, the package genhttp-modules-protobuf adds support for
Protocol Buffers (ContentType.ApplicationProtobuf) which is not
enabled by default. It is based on kotlinx.serialization,
so the types sent and received need to be @Serializable. It adds an addProtobuf() extension to the
registry builder, so you don’t need to register the format manually.
Webservices
import kotlinx.serialization.Serializable
import org.genhttp.modules.conversion.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.protobuf.*
import org.genhttp.modules.reflection.*
import org.genhttp.modules.webservices.*
@Serializable
data class Book(val id: Int, val title: String)
val registry = Serialization.default()
.addProtobuf()
val api = Layout.create()
.addService<BookService>("service", serializers = registry)
class BookService {
@ResourceMethod(Method.Put)
fun store(book: Book): Book = book
}Functional
import kotlinx.serialization.Serializable
import org.genhttp.modules.conversion.*
import org.genhttp.modules.functional.*
import org.genhttp.modules.protobuf.*
@Serializable
data class Book(val id: Int, val title: String)
val registry = Serialization.default()
.addProtobuf()
val api = Inline.create()
.serializers(registry)
.put { book: Book -> book }Controllers
import kotlinx.serialization.Serializable
import org.genhttp.modules.controllers.*
import org.genhttp.modules.conversion.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.protobuf.*
import org.genhttp.modules.reflection.*
@Serializable
data class Book(val id: Int, val title: String)
val registry = Serialization.default()
.addProtobuf()
val api = Layout.create()
.addController<BookController>("controller", serializers = registry)
class BookController {
@ControllerAction(Method.Put)
fun store(book: Book): Book = book
}To configure the default serialization formats, you can pass a Jackson JsonMapper directly to the
default() factory. Start with JsonFormat.getDefaultOptions() to keep the settings genhttp-kt uses
by default and change what you need:
import org.genhttp.modules.conversion.Serialization
import org.genhttp.modules.conversion.serializers.json.JsonFormat
import tools.jackson.databind.SerializationFeature
val mapper = JsonFormat.getDefaultOptions()
.enable(SerializationFeature.INDENT_OUTPUT)
.build()
val serialization = Serialization.default(jsonMapper = mapper)To produce the same output as GenHTTP, the default JSON settings write property names in camel case,
leave out properties that are null, ignore unknown properties when reading, write enums as numbers and
escape HTML-sensitive and non-ASCII characters. XML element names are written in Pascal case and read
case-insensitively.
Interceptors
With interceptors, you can add behavior on operation-level by using additional annotations. This is useful
for features such as authorization. An interceptor implements OperationInterceptor, and an annotation class
annotated with @InterceptWith connects it with the functions it should intercept. As lambdas cannot be
annotated, functional handlers use a function reference.
Webservices
import org.genhttp.api.content.ProviderException
import org.genhttp.api.protocol.ByteString
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.ResponseStatus
import org.genhttp.engine.internal.Host
import org.genhttp.modules.layouting.*
import org.genhttp.modules.reflection.*
import org.genhttp.modules.reflection.operations.Operation
import org.genhttp.modules.webservices.*
suspend fun main() {
val app = Layout.create().addService<MyService>("service")
Host.create()
.handler(app)
.run()
}
@Target(AnnotationTarget.FUNCTION)
@InterceptWith(LocalhostInterceptor::class)
annotation class RequireLocalhost
class LocalhostInterceptor : OperationInterceptor {
override fun configure(attribute: Annotation) {
if (attribute is RequireLocalhost) {
// ...
}
}
override suspend fun intercept(request: Request, operation: Operation, arguments: Map<ByteString, Any?>): InterceptionResult? {
if (request.client.address?.isLoopbackAddress != true) {
throw ProviderException(ResponseStatus.Forbidden, "Access from localhost only!")
}
return null
}
}
class MyService {
@ResourceMethod
@RequireLocalhost
fun myMethod(): String = "Hello to localhost"
}Functional
import org.genhttp.api.content.ProviderException
import org.genhttp.api.protocol.ByteString
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.ResponseStatus
import org.genhttp.engine.internal.Host
import org.genhttp.modules.functional.*
import org.genhttp.modules.reflection.*
import org.genhttp.modules.reflection.operations.Operation
suspend fun main() {
val app = Inline.create().get(::hello)
Host.create()
.handler(app)
.run()
}
@RequireLocalhost
fun hello(): String = "Hello to localhost!"
@Target(AnnotationTarget.FUNCTION)
@InterceptWith(LocalhostInterceptor::class)
annotation class RequireLocalhost
class LocalhostInterceptor : OperationInterceptor {
override fun configure(attribute: Annotation) {
if (attribute is RequireLocalhost) {
// ...
}
}
override suspend fun intercept(request: Request, operation: Operation, arguments: Map<ByteString, Any?>): InterceptionResult? {
if (request.client.address?.isLoopbackAddress != true) {
throw ProviderException(ResponseStatus.Forbidden, "Access from localhost only!")
}
return null
}
}Controllers
import org.genhttp.api.content.ProviderException
import org.genhttp.api.protocol.ByteString
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.ResponseStatus
import org.genhttp.engine.internal.Host
import org.genhttp.modules.controllers.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.reflection.*
import org.genhttp.modules.reflection.operations.Operation
suspend fun main() {
val app = Layout.create().addController<MyController>("controller")
Host.create()
.handler(app)
.run()
}
@Target(AnnotationTarget.FUNCTION)
@InterceptWith(LocalhostInterceptor::class)
annotation class RequireLocalhost
class LocalhostInterceptor : OperationInterceptor {
override fun configure(attribute: Annotation) {
if (attribute is RequireLocalhost) {
// ...
}
}
override suspend fun intercept(request: Request, operation: Operation, arguments: Map<ByteString, Any?>): InterceptionResult? {
if (request.client.address?.isLoopbackAddress != true) {
throw ProviderException(ResponseStatus.Forbidden, "Access from localhost only!")
}
return null
}
}
class MyController {
@ControllerAction(Method.Get)
@RequireLocalhost
fun myMethod(): String = "Hello to localhost"
}An instance of the interceptor is created for every function it is declared on and configured with the
annotation found there, so it can read the arguments of the annotation in configure (@InterceptWith can
also be placed on a function directly). Instead of throwing an exception, intercept can also
return an InterceptionResult with a payload - adjusted like a result - to answer the request
without invoking the function.