JVM and .NET HTTP Frameworks
Two managed-runtime examples that govern the same GET /hello / POST /echo contract through a local Chio sidecar. Spring Boot uses a servlet filter; ASP.NET Core uses a middleware in the standard pipeline.
What it shows
io.backbay.chio:chio-spring-boot: aChioFilterregistered as aFilterRegistrationBeanat highest precedence.Backbay.Chiofor ASP.NET Core:AddChioProtection()plusUseChioProtection().- Both bind to a local
chio api protectsidecar over HTTP. No FFI, no embedded kernel. - Request bodies remain readable by the controller after Chio hashing. Receipt ids appear on the response header path for governed routes.
Binding pattern
Prerequisites
- Spring Boot: a JDK 17+ (the example uses Kotlin 2.3 + Spring Boot 3.2). The Gradle wrapper at
sdks/jvm/gradlewhandles the rest. - .NET: the .NET 8 SDK. The example pulls
ChioMiddlewareas a project reference. - The
chioCLI onPATH. The smokes start a localchio trust serveandchio api protect.
Run them
cd examples/hello-spring-boot # or hello-dotnet
./run.sh
# Full smoke (sidecar + trust + deny + allow)
./smoke.shDefault ports:
| Example | Env var | Default |
|---|---|---|
hello-spring-boot | Spring Boot default config | 8080 |
hello-dotnet | HELLO_DOTNET_PORT | 8019 |
Spring Boot
The integration is one bean. Build a ChioFilter with the sidecar URL, wrap it in a FilterRegistrationBean, and give it Ordered.HIGHEST_PRECEDENCE so it runs before any business filter.
Build file
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.2"))
implementation("io.backbay.chio:chio-spring-boot:0.1.0")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
}Application
package example.hello
import io.backbay.chio.ChioFilter
import io.backbay.chio.ChioFilterConfig
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.core.Ordered
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
@SpringBootApplication
class HelloSpringBootApplication {
@Bean
fun chioFilterRegistration(): FilterRegistrationBean<ChioFilter> {
val filter = ChioFilter(
ChioFilterConfig(
sidecarUrl = System.getenv("CHIO_SIDECAR_URL") ?: "http://127.0.0.1:9090",
),
)
return FilterRegistrationBean<ChioFilter>().apply {
setFilter(filter)
addUrlPatterns("/*")
order = Ordered.HIGHEST_PRECEDENCE
}
}
}
@RestController
class HelloController {
@GetMapping("/healthz")
fun healthz(): Map<String, String> = mapOf("status" to "ok")
@GetMapping("/hello")
fun hello(): Map<String, String> = mapOf("message" to "hello from spring-boot")
@PostMapping("/echo", consumes = [MediaType.APPLICATION_JSON_VALUE])
fun echo(@RequestBody payload: EchoRequest): Map<String, Any> = mapOf(
"message" to payload.message,
"count" to payload.count,
)
}
data class EchoRequest(val message: String, val count: Int = 1)
fun main(args: Array<String>) {
runApplication<HelloSpringBootApplication>(*args)
}Notes: ChioFilter wraps the servlet request to cache the body bytes for replay; the @RequestBody binding still works on echo after the filter has hashed the bytes. The receipt id is set on the response header path; controllers do not need to thread it through manually.
ASP.NET Core
Two lines: register the service, add the middleware. The example uses ASP.NET's minimal API style.
Project file
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../sdks/dotnet/ChioMiddleware/src/ChioMiddleware.csproj" />
</ItemGroup>
</Project>Program
using Backbay.Chio;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddChioProtection();
var app = builder.Build();
app.UseChioProtection();
app.MapGet("/healthz", () => Results.Json(new { status = "ok" }));
app.MapGet("/hello", () => Results.Json(new { message = "hello from dotnet" }));
app.MapPost("/echo", (EchoRequest payload) =>
Results.Json(new
{
message = payload.Message,
count = payload.Count,
}));
app.Run();
internal sealed record EchoRequest(string Message, int Count = 1);Notes: UseChioProtection() installs the middleware near the start of the pipeline. Body bytes are buffered with EnableBuffering()-style semantics so the model binder can re-read them. Configure the sidecar URL via standard ASP.NET configuration (env var, appsettings, or DI options).
./run.sh
# starts dotnet on http://127.0.0.1:8019 (override with HELLO_DOTNET_PORT)Critical wiring
One bean (Spring) and one middleware call (.NET). These are the lines that flip on Chio enforcement.
@Bean
fun chioFilterRegistration(): FilterRegistrationBean<ChioFilter> {
val filter = ChioFilter(ChioFilterConfig(sidecarUrl = ...))
return FilterRegistrationBean<ChioFilter>().apply {
setFilter(filter)
addUrlPatterns("/*")
order = Ordered.HIGHEST_PRECEDENCE
}
}builder.Services.AddChioProtection();
var app = builder.Build();
app.UseChioProtection();Smoke assertions
Both smokes drive the same three-call pattern: GET allow, POST deny without capability, POST allow with capability. The deny body for Spring and .NET both carry error: chio_access_denied and a receipt_id field.
# /hello
assert body["message"] == "hello from spring-boot", body
# /echo without token (403)
assert body["error"] == "chio_access_denied", body
assert body["receipt_id"], body
# /echo with X-Chio-Capability
assert body["message"] == "hello", body
assert body["count"] == 2, body# Same shape: chio_access_denied + receipt_id on deny
assert body["message"] == "hello from dotnet", body
assert body["error"] == "chio_access_denied", body
assert body["receipt_id"], bodyInspect after
cd .artifacts/$(ls -t .artifacts | head -1)
# Receipt id arrives as x-chio-receipt-id (lower-case)
grep -i x-chio-receipt-id hello.headers deny.headers allow.headers
# 3 persisted receipts in the sidecar SQLite store
wc -l receipts.ndjson # expect: 3
jq -r '.verdict.outcome' receipts.ndjson | sort | uniq -c
# expect: 2 allow, 1 deny
# Confirm the deny receipt id matches the one in deny.json
jq -r '.receipt_id' deny.json
grep -i x-chio-receipt-id deny.headers
# Direct SQLite peek
sqlite3 state/sidecar-receipts.sqlite3 \
"select id, route_method, route_path from receipts order by created_at;"When this fits
chio api protect in front of it instead. See OpenAPI Sidecar.JVM vs .NET, in one table
| Aspect | Spring Boot | ASP.NET Core |
|---|---|---|
| SDK package | io.backbay.chio:chio-spring-boot | Backbay.Chio (project reference) |
| Pipeline shape | Servlet filter via FilterRegistrationBean | ASP.NET middleware |
| Order | Ordered.HIGHEST_PRECEDENCE | Order of UseChioProtection() in Program.cs |
| Body reuse | Wrapped servlet request | Stream buffering on HttpRequest |
| Sidecar URL | CHIO_SIDECAR_URL env | ASP.NET config (env, appsettings) |
Why HTTP and not FFI
Both runtimes have mature FFI surfaces (JNI, P/Invoke), but the sidecar shape wins on three counts:
- No native build per platform: the Java archive and the .NET assembly stay pure managed code; the sidecar binary handles platform-specific concerns.
- Single source of truth: the sidecar holds policy, kernel state, and receipts. Multiple managed processes can share one sidecar.
- Crash isolation: a bug in the kernel cannot bring down the JVM or the .NET host.
The cost is one localhost roundtrip per evaluated request, which the sidecar amortizes well in practice.
Next
- HTTP Framework Middleware
- Protect an API: the zero-code reverse-proxy alternative.
- Go and C++ HTTP Frameworks, Node HTTP Frameworks, Python HTTP Frameworks
- Examples Overview