Deviations
genhttp-kt keeps the API and the behavior of GenHTTP. Where the JVM, Kotlin or a library requires a different solution, the port says so - this page lists every such place, grouped by the area you will meet it in. The complete log is DEVIATIONS.md in the repository.
The one you are most likely to trip over: Kotlin does not allow annotations or default values
on lambda parameters. A functional handler that needs @FromBody or an optional parameter is
written as a function reference (post(::create)), see
Coming from GenHTTP.
Naming and API Shape
| Deviation | Reason |
|---|---|
Interfaces drop the I prefix (Handler, Request). Static factories that would clash with an interface become companion extensions (Resource.fromString(...)), implementations that would clash get a descriptive prefix (HttpRequest, AbstractServerHost, MapPropertyBag). | Kotlin conventions |
The Async suffix is dropped, such methods are suspend functions. | Kotlin coroutines |
IHandlerBuilder<TBuilder> is HandlerBuilder.WithConcerns<TBuilder>. | Kotlin cannot overload an interface name by its number of type parameters |
Forwarding.For is Forwarding.forAddress. | for is a keyword in Kotlin |
Annotations that would clash with API types get an Api prefix: @ApiRequestBody, @ApiRequestHeader, @ApiResponse, @ApiResponseHeader. DocumentationAttribute is the meta-annotation @Documentation. | Clashes with RequestBody, Response, …; annotations cannot inherit |
Optional annotation parameters use "" or Nothing::class instead of null. | Annotation parameters cannot be nullable |
ResponseStatus carries its numeric code, ResponseStatus.fromCode(Int) replaces casts. HttpProtocols offers hasFlag, plus and minus. | Kotlin enums have no implicit integer value or flag arithmetic |
SslProtocols, SslPolicyErrors, X509RevocationMode and CompressionLevel are small enums of the API, flags are a Set. | There is no equivalent in the JDK |
IServer.DisposeAsync() is suspend fun close(), an EndPoint is AutoCloseable. | Kotlin lifecycle conventions |
IPropertyBag.TryGet<T>(key, out T) is tryGet(key, KClass<T>): T?, with a reified tryGet<T>(key). | No out parameters |
Comparing a MemoryView with raw memory uses contentEquals(ByteBuffer). | == in Kotlin only compares with Any? |
MemoryView.copyTo(ByteBuffer) copies the content of a view. | bytes returns a new buffer on every access, upstream exposes the memory directly |
The views (ByteString, ContentType, …) are classes that compute their hash on first use. | The JVM has no user-defined value types |
ResponseBuilder.apply(modifications) keeps its name from GenHTTP and so shadows Kotlin’s apply { } for response builders. | The upstream name is kept |
Engines and Hosting
| Deviation | Reason |
|---|---|
Kestrel is not ported, the counterpart of ioxide is the Netty engine (genhttp-core-netty, org.genhttp.engine.netty.Host); ServerEngine is Internal, Netty or Custom. | Kestrel and ioxide are specific to .NET; Netty is the JVM counterpart of ioxide |
| The Netty engine serves HTTP/1.0 and HTTP/1.1 only (plain and TLS, including websockets), there is no native file path. | Scope of its first version |
The EngineOptions of the Netty engine tune Netty: EventLoopOptions(count, transport) replaces ReactorOptions, TcpTransportOptions keeps the handshake timeout, the cipher options, the listen backlog and the idle and send timeouts and adds tlsProvider; there are no kTLS, write slab, pool, zero copy, QUIC and HTTP/3 options; onReactorStart is onEventLoopStart. | The options of ioxide tune its io_uring runtime |
| The Netty engine receives request bodies completely before the handler runs (at most 512 MB, larger ones are answered with 413), unterminated request headers above 1 MB are answered with 431. | RequestBody.asStream() is a blocking stream, which cannot wait for data on the event loop that has to deliver it |
The Netty engine terminates TLS with OpenSSL (BoringSSL) or the JDK; every CertificateValidator works, cipherSuites (TLS 1.3) only applies to the JDK provider; certificates are not reloaded by a ReloadCertificates call, the provider is asked for every handshake. | The TLS providers Netty offers |
The Netty engine reports GenHTTP/<version> in the Server header (ioxide: ioxide-genhttp), the state of connections is pooled per event loop. | The engines share their response handling |
| The read timeouts of the internal engine are checked once per second, so they fire up to a second late. | A timer per read would cost more than the deadline it enforces |
The Date header is rendered once per second by a background thread. | Reading the clock costs about 30 ns per response on virtualized hosts |
| There is no ASP.NET Core adapter. | ASP.NET Core does not exist on the JVM |
An IPv6 endpoint with dualStack = false accepts IPv4 connections but closes them immediately. | The JDK cannot set IPV6_V6ONLY |
FileAssetsBuilder.refreshInterval(...) is accepted but has no effect. | It only affects the Ioxide engine in GenHTTP |
I/O and Memory
| Deviation | Reason |
|---|---|
Stream is InputStream or OutputStream, IBufferWriter<byte> is a WritableByteChannel, the PipeReader of an upgraded connection an InputStream. | The JVM’s standard types, usable from coroutines because connections run on virtual threads |
On the Netty engine, the stream of an upgraded connection is also a SuspendingInput (an addition to the API): content that reads it with readSuspending on its dispatcher - such as the websockets module - runs on the event loop. | A stream of the JVM blocks, which would halt the event loop |
ReadOnlyMemory<byte> is a read-only ByteBuffer whose content is position until limit. | The JDK’s standard memory view |
WritingStream is a write-only OutputStream. | Streams of the JVM are not bidirectional |
Checksums of stream content require markSupported(), the length of StreamContent is only the known length. | Streams of the JVM cannot seek |
FormattableContent<T> is FormattableContent(value: Any). | No overloading by generic arity |
The sync and async overloads of Handler.from and Concern.from are one suspend lambda. | A suspend lambda accepts both |
A directory container rejects paths that escape its directory, e.g. via %2F. | Hardening |
Reflection and Frameworks
| Deviation | Reason |
|---|---|
Operations are analyzed and invoked with kotlin-reflect instead of Roslyn code generation; inline handlers take suspend lambdas or function references. | See Execution Modes |
| Default values of parameters cannot be read: an optional parameter is left out of the call so that Kotlin’s own default applies. | A limitation of kotlin-reflect |
The types of lambda parameters are converted with KType.toJavaType() for serialization. | kotlin-reflect cannot map them to Java types directly |
Dependency injection uses Koin instead of Microsoft.Extensions.DependencyInjection: IServiceProvider is Koin, scoped services are registered in scope(RequestScope) { scoped { } }. | Koin is the established container for Kotlin |
Serialization and Templates
| Deviation | Reason |
|---|---|
JSON escapes HTML-sensitive and non-ASCII characters, enums with explicit values (ResponseStatus) are written as their value, whole-number floating point values without fraction (42). | Mirrors the defaults of System.Text.Json |
| XML element names are written in PascalCase and read case-insensitively. | Mirrors XmlSerializer and the property names of .NET |
Templates of Renderer.from(...) and of the API browsers use Pebble syntax ({{ title }}) instead of Cottle. | There is no Cottle for the JVM |
Security
| Deviation | Reason |
|---|---|
A certificate with its private key is a KeyStore.PrivateKeyEntry instead of an X509Certificate2. | The JDK keeps keys and certificates apart |
Bearer tokens are com.nimbusds.jwt.SignedJWT, keys are java.security.Key; unsigned (alg: none) and encrypted tokens are rejected. | Nimbus JOSE + JWT replaces System.IdentityModel; hardening |
Lambda overloads of authentication(...) with two parameters need explicit parameter types. | Kotlin does not resolve overloads by the lambda body |
Localization
| Deviation | Reason |
|---|---|
The selected culture is read with CurrentCulture.culture() and uiCulture() (a coroutine context element) instead of CultureInfo.CurrentCulture. | The JVM has no culture per thread, and coroutines may switch threads |
Overloads of fromRequest are told apart by the return type of their lambda; the culture variant is fromRequestCultures. | Kotlin cannot overload lambdas by return type otherwise |
Language tags without a region resolve to a default region (de to de-DE). | Mirrors CultureInfo.CreateSpecificCulture |
Real Time
| Deviation | Reason |
|---|---|
A WebsocketConnection is not disposable; cancellation throws CancellationException instead of a Canceled frame error; segmented payloads are linearized into a copy. | close() would clash with the close frame, and the engine owns the input |
Status codes of close frames are Int (0 to 65535), opcodes Byte. | No unsigned types in the public API |
An EventStream rethrows CancellationException instead of sending a retry. | Structured concurrency |
| On the Netty engine, the message pump of a websocket runs on the event loop of its connection, so websocket handlers must not block. | Websockets cost as little CPU per message as on the internal engine |
Proxies and Load Balancing
| Deviation | Reason |
|---|---|
The JDK HttpClient replaces .NET’s: adjustHandler takes (HttpClient.Builder) -> Unit, adjustClient runs on the built client, the read timeout applies per request. | The JDK client is immutable and has no handler type |
Expect, Transfer-Encoding and Content-Length request headers are not copied; the JDK client adds a User-Agent if the client sent none. | Restricted headers of the JDK client |
WebsocketTunnelContent takes the request as a second argument; the tunnel ends when either direction ends. | The client side is read from request.upgrade() |
Status codes unknown to ResponseStatus are answered with 502, Content-* request headers are matched case-insensitively. | Avoids invalid responses for unusual upstreams |
PriorityEvaluation is a type alias for (Request) -> Priority. | Delegates map to function types |
Parser
| Deviation | Reason |
|---|---|
The Glyph11 parser reads contiguous ByteBuffers only. | The engine always provides a contiguous buffer |
| The views on the fields of a parsed request are recycled for the next request of the connection. | Views are objects on the JVM, recycling them keeps parsing allocation-free |
A few tests could not be ported literally for the same reasons - they are listed in the log in the repository, too. None of them is weakened: each checks the same behavior in the way the platform allows.