Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ lazy val zioHttpTestkit = (project in file("zio-http-testkit"))
testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"),
libraryDependencies ++= netty ++ Seq(
`zio`,
`zio-test`,
"dev.zio" %% "zio-test" % ZioVersion,
`zio-test-sbt`,
),
)
Expand Down
125 changes: 125 additions & 0 deletions docs/concepts/dev-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Dev / Preprod / Prod Modes

ZIO HTTP provides a simple built-in notion of application "mode" so you can adapt behavior (e.g. enable extra diagnostics in development, stricter settings in production, other routes, different error handling) without wiring your own config keys everywhere.

The available modes are:

- `Mode.Dev` (default if nothing is configured)
- `Mode.Preprod` (a staging / pre‑production environment)
- `Mode.Prod` (production)

## Reading the Current Mode

Use any of the following helpers:

```scala
import zio.http.Mode

// Full value
def m: Mode = Mode.current

// Convenience booleans
val isDev = Mode.isDev
val isPreprod = Mode.isPreprod
val isProd = Mode.isProd
```

## Configuring the Mode

The mode is determined in this precedence order:

1. JVM System Property: `-Dzio.http.mode=<dev|preprod|prod>`
2. Environment Variable: `ZIO_HTTP_MODE=<dev|preprod|prod>`
3. Fallback: `dev`

Examples:

```bash
# Using a JVM system property
sbt "run -Dzio.http.mode=preprod"

# Using an environment variable (takes effect if the system property is NOT set)
ZIO_HTTP_MODE=prod sbt run
```

Unknown values cause a warning on stderr and the mode falls back to `dev`:

## Typical Use Cases

You can branch on the mode to enable / disable features:

```scala
import zio._
import zio.http._

val extraRoutes: Routes[Any, Nothing] =
if (Mode.isDev) SwaggerUI.routes("docs", OpenAPIGen.empty)
else Routes.empty

val baseRoutes: Routes[Any, Nothing] = Routes(
Method.GET / "health" -> handler(Response.ok)
)

val appRoutes = baseRoutes ++ extraRoutes
```

Or adapt server config:

```scala
val serverConfig =
if (Mode.isProd) Server.Config.default
.leakDetection(false)
.requestDecompression(true)
else Server.Config.default
.leakDetection(true) // extra visibility in dev
.maxThreads(4) // keep lighter in local dev
```

## Testing Modes

Inside tests you generally want to *temporarily* switch the mode to verify conditional behavior. The testkit provides aspects in `zio.http.HttpTestAspect`:

- `HttpTestAspect.devMode`
- `HttpTestAspect.preprodMode`
- `HttpTestAspect.prodMode`

Each aspect sets the mode for the duration of the test, restoring the previous mode afterward. This allows you to write tests that depend on specific modes without affecting other tests.

Example:

```scala
import zio.test._
import zio.http._

object ModeExamplesSpec extends ZIOSpecDefault {
def spec = suite("ModeExamplesSpec")(
test("enables preprod logic") {
assertTrue(Mode.current == Mode.Preprod)
} @@ HttpTestAspect.preprodMode,

test("enables prod logic") {
assertTrue(Mode.isProd)
} @@ HttpTestAspect.prodMode,
) @@ TestAspect.sequential // IMPORTANT, see below
}
```

### Why `TestAspect.sequential`?

The mode is stored per JVM. When you apply different mode aspects to multiple tests in the **same suite**, running them in parallel could cause races (e.g. one test reads prod while another just switched to preprod). Adding `@@ TestAspect.sequential` ensures the suite’s tests execute one after another so each mode override is isolated.

If every test suite uses only one mode (or you wrap all tests in a single aspect at the suite level), sequential execution is not strictly necessary. It is required only when multiple tests in the same suite each apply different mode aspects.

## Quick Reference

| Task | How |
|------|-----|
| Read current mode | `Mode.current` |
| Check if dev | `Mode.isDev` |
| Run in preprod | `-Dzio.http.mode=preprod` or `ZIO_HTTP_MODE=preprod` |
| Override in a test | `test("...") { ... } @@ HttpTestAspect.prodMode` |
| Avoid race conditions | Apply `@@ TestAspect.sequential` to suite when multiple mode aspects are used |

## When *Not* to Use Mode

For complex environment-dependent configuration (database URLs, secrets, feature flags) prefer a dedicated configuration service (e.g. `zio-config`).
2 changes: 1 addition & 1 deletion website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const sidebars = {
type: "category",
collapsed: false,
label: "Concepts",
items: ["concepts/routing", "concepts/middleware", "concepts/endpoint"],
items: ["concepts/routing", "concepts/middleware", "concepts/endpoint", "concepts/dev-mode"],
},
],

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import sttp.tapir.{Endpoint => TEndpoint, endpoint => tendpoint, path => tpath,
// [info] EndpointBenchmark.benchmarkSmallDataZioCollect thrpt 2 701.566 ops/s

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class EndpointBenchmark {
// implicit val actorSystem: ActorSystem = ActorSystem("api-benchmark-actor-system")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import zio.http._
import org.openjdk.jmh.annotations._

@State(org.openjdk.jmh.annotations.Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class FormToQueryBenchmark {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import zio.http.endpoint.Endpoint
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class MethodLookupBenchmark {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import zio.http._
import org.openjdk.jmh.annotations._

@State(org.openjdk.jmh.annotations.Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class RoundtripBenchmark {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import zio.http.{Handler, Method, Request, Routes}
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class RoutesBenchmark {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import zio.http.internal.DateEncoding
import org.openjdk.jmh.annotations._

@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.AverageTime))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.AverageTime))
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Threads(16)
@Fork(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import org.openjdk.jmh.annotations._

@nowarn
@State(org.openjdk.jmh.annotations.Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 3, time = 3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import zio.http.{Cookie, Path}
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class CookieDecodeBenchmark {
val random = new scala.util.Random()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import zio.http._
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class HttpCollectEval {
private val MAX = 10000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import zio.http._
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class HttpCombineEval {
private val req = Request.get("/foo")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import zio.http._
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class HttpNestedFlatMapEval {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import zio._

import zio.http._

import org.openjdk.jmh.annotations.{Scope => JScope, _}
import org.openjdk.jmh.annotations.{Mode, Scope => JScope, _}

@State(JScope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class HttpRouteTextPerf {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import zio.http.MediaType
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class ProbeContentTypeBenchmark {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import zio.http.Scheme
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class SchemeDecodeBenchmark {
private val MAX = 1000000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.openjdk.jmh.annotations._
import sttp.client3.{HttpURLConnectionBackend, UriContext, basicRequest}

@State(org.openjdk.jmh.annotations.Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class ServerInboundHandlerBenchmark {
private val random = scala.util.Random
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.netty.handler.codec.http.DefaultHttpHeaders
import org.openjdk.jmh.annotations._

@State(Scope.Thread)
@BenchmarkMode(Array(Mode.AverageTime))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.AverageTime))
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(1)
@Warmup(iterations = 3, time = 3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import io.netty.handler.codec.DateFormatter
import org.openjdk.jmh.annotations._

@State(org.openjdk.jmh.annotations.Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class DateEncodingBenchmark {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import zio.http.netty.NettyQueryParamEncoding
import org.openjdk.jmh.annotations._

@State(org.openjdk.jmh.annotations.Scope.Thread)
@BenchmarkMode(Array(Mode.Throughput))
@BenchmarkMode(Array(org.openjdk.jmh.annotations.Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
class QueryEncodingBenchmark {

Expand Down
26 changes: 26 additions & 0 deletions zio-http-testkit/src/main/scala/zio/http/HttpTestAspect.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package zio.http

import zio._
import zio.test._

object HttpTestAspect {

private def withMode(mode: Mode): TestAspectAtLeastR[Scope] =
TestAspect.aroundWith(
ZIO.succeed {
val previous = Mode.current
java.lang.System.setProperty("zio.http.mode", mode.toString)
previous
},
)((restorePrevious: Mode) => ZIO.succeed(java.lang.System.setProperty("zio.http.mode", restorePrevious.toString)))

val devMode: TestAspectAtLeastR[Scope] =
withMode(Mode.Dev)

val prodMode: TestAspectAtLeastR[Scope] =
withMode(Mode.Prod)

val preprodMode: TestAspectAtLeastR[Scope] =
withMode(Mode.Preprod)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package zio.http

import zio.test._

object HttpTestAspectSpec extends ZIOSpecDefault {
def spec = suite("HttpTestAspectSpec")(
test("Preprod is enabled vai test aspect") {
assertTrue(Mode.current == Mode.Preprod)
} @@ HttpTestAspect.preprodMode,
test("Prod is enabled via test aspect") {
assertTrue(Mode.current == Mode.Prod)
} @@ HttpTestAspect.prodMode,
) @@ TestAspect.sequential
}
11 changes: 11 additions & 0 deletions zio-http/jvm/src/test/scala/zio/http/ModeSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package zio.http

import zio.test._

object ModeSpec extends ZIOSpecDefault {
override def spec = suite("ModeSpec")(
test("Mode should be Test") {
assertTrue(Mode.Dev.isActive)
},
)
}
Loading
Loading