BuildHTTP Frameworks
JVM and .NET HTTP Frameworks
Protect Spring Boot and ASP.NET Core routes through a local Chio sidecar over HTTP.
What it shows
world.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("world.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 world.chio.ChioFilter
import world.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("/hello", "/echo")
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. ChioFilterConfig has no skip or exclude list, so the registration governs exactly the two URL patterns it names. Registering on /hello and /echo is what keeps /healthz out of the sidecar path.
ASP.NET Core
Register the service, then gate the middleware so it covers route except the readiness probe. 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;
namespace HelloDotnet;
internal static class HelloApp
{
internal static WebApplication Create(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddChioProtection();
var app = builder.Build();
// /healthz is mapped before the guard and left out of it, so the
// readiness probe never depends on the sidecar.
app.MapGet("/healthz", () => Results.Json(new { status = "ok" }));
app.UseWhen(
context => RequiresChioProtection(context.Request.Path),
branch => branch.UseChioProtection());
app.MapGet("/hello", () => Results.Json(new { message = "hello from dotnet" }));
app.MapPost("/echo", (EchoRequest payload) =>
Results.Json(new
{
message = payload.Message,
count = payload.Count,
}));
return app;
}
internal static bool RequiresChioProtection(PathString path) =>
!path.Equals("/healthz", StringComparison.OrdinalIgnoreCase);
}
internal sealed record EchoRequest(string Message, int Count = 1);Notes: app.UseWhen(...) runs UseChioProtection() only on paths where RequiresChioProtection returns true, which excludes /healthz. 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("/hello", "/echo")
order = Ordered.HIGHEST_PRECEDENCE
}
}builder.Services.AddChioProtection();
var app = builder.Build();
app.UseWhen(
context => RequiresChioProtection(context.Request.Path),
branch => branch.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.verdict' 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, json_extract(receipt_json, '$.method') as method, json_extract(receipt_json, '$.route_pattern') as route_pattern from http_receipts order by rowid;"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 | world.chio:chio-spring-boot | Backbay.Chio (project reference) |
| Pipeline shape | Servlet filter via FilterRegistrationBean | ASP.NET middleware |
| Order | Ordered.HIGHEST_PRECEDENCE | UseWhen placement in HelloApp.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.
- Shared state: 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 adds one localhost round trip per evaluated request.
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