Skip to content

Actions

Actions are Misk’s unit for an endpoint. Misk lets you make HTTP actions, and gRPC actions via Wire.

Web Actions

Below are some example Web action declarations. Note that many of the annotations are optional.

Calls are authenticated at the service-level (service is listed in the @Authenticated annotation) or at the user-level (user has at least one of the capabilities listed in the @Authenticated annotation).

GET:

@Singleton
class HelloWebAction @Inject constructor() : WebAction {
  @Get("/hello/{name}") // Enclose path parameters in {}
  @ResponseContentType(MediaTypes.APPLICATION_JSON)
  @Authenticated(services = ["my-other-app"], capabilities = ["my-app_owners"])
  fun hello(
    // Use @PathParam with the name of the param. Required if there's a param in the path pattern.
    @PathParam name: String,
    // RequestHeaders is optional:
    @RequestHeaders headers: Headers,
    // QueryParams are optional:
    @QueryParam nickName: String?, // e.g. /hello/abc?nickName=def
    @QueryParam greetings: List<String>? // e.g. /hello/abc?greetings=def&greetings=ghi
  ): HelloResponse {
    return HelloResponse(name)
  }
}

POST:

@Singleton
class HelloWebPostAction @Inject constructor() : WebAction {
  @Post("/hello/{name}")
  @RequestContentType(MediaTypes.APPLICATION_JSON)
  @ResponseContentType(MediaTypes.APPLICATION_JSON)
  @Authenticated(services = ["my-other-app"], capabilities = ["my-app_owners"])
  fun hello(
      @PathParam name: String, 
      // RequestBody is optional, and is automatically deserialized to the provided type.
      @RequestBody body: PostBody
  ): HelloPostResponse {
    return HelloPostResponse(body.greeting, name)
  }
}

data class HelloPostResponse(val greeting: String, val name: String)

data class PostBody(val greeting: String)

Setting up HTTP actions

Install the action into a module:

class HelloModule : KAbstractModule() {
  override fun configure() {
    install(WebActionModule.create<HelloWebAction>())
    install(WebActionModule.create<HelloWebPostAction>())
  }
}

And then put that module onto the top level MiskApplication.

fun main(args: Array<String>) {
  MiskApplication(
    // ...
    HelloModule(), // new!
  ).run(args)
}

Response type

If you change the action’s response type to Response<T>, it gives you better control over the response status code and headers.

@Singleton
class HelloWebResponseAction @Inject constructor() : WebAction {
  @Get("/hello_but_203/{name}")
  @ResponseContentType(MediaTypes.APPLICATION_JSON)
  fun hello(@PathParam name: String): Response<HelloResponse> = Response(
    statusCode = 203,
    headers = headersOf(),
    body = HelloResponse()
  )
}

It’s also possible to throw exceptions that are mapped to status codes.

@Singleton
class HelloWebResponseAction @Inject constructor() : WebAction {
  @Get("/no_access/{name}")
  fun hello(@PathParam name: String): HelloResponse {
      throw UnauthenticatedException()
  }
}

gRPC Actions

Misk has support for gRPC actions via the Wire protocol buffer (protobuf) library.

To create a gRPC action, first define the relevant protos for your service. Let’s say we’re creating a GreeterService that exposes one API, Hello. Create this file in src/main/proto/hello.proto:

syntax = "proto2";
package squareup.cash.hello;

option java_package = "com.squareup.protos.cash.hello";

message HelloRequest {
  optional string message = 1;
}

message HelloResponse {
  optional string message = 1;
}

service GreeterService {
  rpc Hello(HelloRequest) returns (HelloResponse) {}
}

Next, in your project’s build file (for this example, build.gradle.kts), add a dependency on the wire plugin:

plugins {
  id("com.squareup.wire")
}

Add the following configuration to generate the gRPC interfaces for your service:

wire {
  sourcePath {
    srcDir("src/main/proto")
  }

  kotlin {
    include("squareup.cash.hello.GreeterService")
    rpcCallStyle = "blocking"
    rpcRole = "server"
    singleMethodServices = true
  }

  java {
  }
}

Finally, implement and bind your gRPC action. GreeterServiceHelloBlockingServer is generated by Wire.

@Singleton
class HelloGrpcAction @Inject internal constructor()
  : GreeterServiceHelloBlockingServer, WebAction {

  @Unauthenticated
  override fun Hello(request: HelloRequest): HelloResponse {
    return HelloResponse("message")
  }
}

// This module binds HelloGrpcAction.
class GreeterActionModule : KAbstractModule() {
  override fun configure() {
    install(WebActionModule.create<HelloGrpcAction>())
  }
}

Misk also supports rpcCallStyle = "suspending" for suspending gRPC actions. This is the way to generate server actions if you intend to use coroutines to implement the business logic of your action. The generated interface will then expect you to implement a suspending function instead of a regular blocking function for your action’s handler method. See coroutines for more information.

wire {
  sourcePath {
    srcDir("src/main/proto")
  }

  kotlin {
    include("squareup.cash.hello.GreeterService")
    rpcCallStyle = "suspending"
    rpcRole = "server"
    singleMethodServices = true
  }

  java {
  }
}

The above Wire Gradle plugin configuration will have a suspending function in the generated interface instead of a regular function. Your implementing class will then look like this:

@Singleton
class HelloGrpcAction @Inject internal constructor()
  : GreeterServiceHelloBlockingServer, WebAction {

  @Unauthenticated
  override suspend fun Hello(request: HelloRequest): HelloResponse {
    return HelloResponse("message")
  }
}

Automatic JSON endpoint

Misk automatically creates a JSON variant for every protobuf-based action. This applies to both gRPC actions (@com.squareup.wire.WireRpc or @misk.web.Grpc) and POST Protobuf actions (@Post with @RequestContentType(MediaTypes.APPLICATION_PROTOBUF)). The JSON variant has the same path and annotations but accepts Content-Type: application/json via plain HTTP POST — no gRPC framing or HTTP/2 required. Request and response bodies are JSON-serialized versions of the protobuf messages. This is useful for debugging, browser-based clients, and environments where gRPC or HTTP/2 is not available.

Unframed requests

Misk web supports serving unary RPC methods (no streaming request or response) without gRPC wire format framing. This can be useful for gradually migrating an existing HTTP POST based API to gRPC. To accept plain HTTP POST with application/x-protobuf (raw protobuf bytes without gRPC framing), add @EnableUnframedRequests to your action’s method.

@Singleton
class HelloGrpcAction @Inject constructor() : GreeterServiceHelloBlockingServer, WebAction {

  @Unauthenticated
  @EnableUnframedRequests
  override fun Hello(request: HelloRequest): HelloResponse {
    return HelloResponse("message")
  }
}

With @EnableUnframedRequests and the Automatic JSON endpoint, a single action accepts all three protocols:

Protocol Content-Type Notes
gRPC application/grpc Standard gRPC with HTTP/2 framing
JSON POST application/json Automatic, always enabled
Protobuf POST application/x-protobuf Opt-in via @EnableUnframedRequests

This is particularly useful for services migrating from Armeria, where unframed protobuf POST is supported out of the box. It eliminates the need to define a separate WebAction class and path for protobuf POST support.

@EnableUnframedRequests only applies to unary gRPC actions (single request parameter). Streaming actions require gRPC framing and are not supported.

Custom HTTP endpoints alongside gRPC

You can also create a separate WebAction class for a custom HTTP endpoint alongside a gRPC action. A common pattern is to have them share a dependency:

@Singleton
class HelloGrpcAction @Inject constructor(val greeter: Greeter)
  : GreeterServiceHelloBlockingServer, WebAction {
  @Unauthenticated override fun Hello(request: HelloRequest) = HelloResponse(greeter.greet())
}

@Singleton
class HelloWebAction @Inject constructor(val greeter: Greeter) : WebAction {
  @Unauthenticated
  @Get("/hello")
  @ResponseContentType(MediaTypes.APPLICATION_JSON)
  fun hello() = HelloResponse(greeter.greet())
}

@Singleton
class Greeter @Inject constructor() {
  fun greet() = "Hello world"
}

ActionScoped

ActionScoped gives an action access to context produced by the action’s interceptors.

Misk has a few ActionScoped items built in:

  • MiskCaller - access derived authorization details
  • HttpCall - access lower level HTTP details, e.g. request headers

Testing

Use tests annotated with @MiskTest to perform tests. There are two common patterns to testing actions:

Test Action classes directly

Make sure that the module under test contains a Guice binding for the action and its dependencies, then inject your action.

class MyModule : KAbstractModule() {
  override fun configure() {
    install(WebActionModule.create<HelloWebAction>())
    // Alternatively, a direct or just-in-time binding might be sufficient.
  }
}

@MiskTest class MyTest {
  @MiskTestModule val module = MyModule()
  @Inject lateinit var action: HelloWebAction

  // use action...
}

Testing ActionScoped

Use @WithMiskCaller for ActionScoped<MiskCaller>:

@MiskTest
@WithMiskCaller(user = "test-user") // or @WithMiskCaller(service = "test-service")
class MyTest {
  @MiskTestModule val module = MyModule()
  @Inject lateinit var action: HelloWebAction // or any other class that injects ActionScoped<MiskCaller>

  // use action...
}

For types other than MiskCaller, use ActionScope directly either within your setup and teardown test methods:

@MiskTest
class MyTest {
  @MiskTestModule val module = MyModule()
  @Inject lateinit var actionScope: ActionScope
  @Inject lateinit var action: HelloWebAction // or any other class that injects ActionScoped<MyScopedObject>

  @BeforeEach fun setUp() {
    actionScope.create(
      mapOf(
        keyOf<MyScopedObject>() to MyScopedObject()
      )
    ).enter()
  }

  @AfterEach fun tearDown() {
    actionScope.close()
  }

  @Test fun test() {
    // use action...
  }
}

…or within the test itself:

@MiskTest
class MyTest {
  @MiskTestModule val module = MyModule()
  @Inject lateinit var actionScope: ActionScope
  @Inject lateinit var action: HelloWebAction

  @Test fun test() {
    actionScope.create(
      mapOf(
        keyOf<MyScopedObject>() to MyScopedObject()
      )
    ).inScope {
      // use action or class which injects ActionScoped<MyScopedObject>...
    }
  }
}

Verifying action registration

A common mistake is implementing a WebAction but forgetting to register it via WebActionModule.create<YourAction>(). Without registration, the action won’t be exposed as an HTTP endpoint.

Misk provides WebActionRegistrationTester to catch this. It scans your service’s packages for WebAction implementations and verifies each has a corresponding WebActionEntry registration.

Using the base class:

@MiskTest
class WebActionRegistrationTest : AbstractWebActionRegistrationTest() {
  @MiskTestModule
  val module: Module = DinoTestModule()

  override fun webActionPackages(): List<String> {
    // Scan your service's packages for WebAction implementations
    return listOf("com.example.dino")
  }

  // Optional: exclude specific actions from verification
  override fun excludeWebAction(actionClass: KClass<out WebAction>): Boolean {
    return actionClass.simpleName in setOf("TestOnlyAction", "SpecialAdminAction")
  }

  // Optional: hint shown in error messages
  override fun registrationModuleHint(): String = "your service's web actions module"
}

Using the utility directly:

@Test
fun allWebActionsAreRegistered() {
  val injector = Guice.createInjector(DinoTestModule())

  WebActionRegistrationTester.assertAllWebActionsRegistered(
    injector,
    WebActionRegistrationTester.Options(
      basePackages = listOf("com.example.dino"),
      excludePredicate = { actionClass ->
        actionClass.simpleName == "SpecialAction"
      },
      registrationModuleHint = "DinoWebModule",
    )
  )
}

When a missing registration is detected, the assertion fails with a helpful error message including copy-paste registration code:

The following WebActions are not registered:
  - com.example.dino.actions.ForgottenAction

Copy and paste the following lines into your WebAction registration module in DinoWebModule:
-----
    install(WebActionModule.create<ForgottenAction>())
or
    install(WebActionModule.create<ForgottenAction>("<optional path prefix>"))
-----

Built-in exclusions:

The following are automatically excluded from verification: - Abstract classes and interfaces - Classes in .api.internal. packages (typically Wire/gRPC-generated actions registered via different mechanisms)

Integration tests

It’s possible to perform tests terminating at the app’s HTTP/gRPC interface.

The module under test should include WebServerTestingModule so that Misk stands up a server during tests:

class MyModule : KAbstractModule() {
   override fun configure(){
    install(WebServerTestingModule())
    // install other modules...
 }
}

Then test HTTP requests using WebTestClient (assertions here were made using Kotest):

@MiskTest(startService = true)
class HelloWebIntegrationTest {
  @Suppress("unused")
  @MiskTestModule val module = MyModule()

  @Inject lateinit var webTestClient: WebTestClient

  @Test
  fun `makes a call to the service`() {
    val response = webTestClient.post("/hello", HelloRequest("world"))
    response.response.code shouldBe 200
    response.parseJson<HelloResponse>() shouldBe HelloResponse("hello world")
  }
}

Test gRPC requests by setting up a gRPC client pointing to the running app:

class MyServerModule : KAbstractModule() {
  override fun configure() {
    install(WebServerTestingModule())
    // Assume RobotLocatorWebAction is a gRPC action.
    install(WebActionModule.create<RobotLocatorWebAction>())
  }
}

class MyClientModule(val jetty: JettyService): KAbstractModule() {
  override fun configure() {
    // Assume RobotLocator was generated via Wire.
    install(GrpcClientModule.create<RobotLocator, GrpcRobotLocator>("robots"))
  }

  @Provides
  @Singleton
  fun provideHttpClientConfig(): HttpClientsConfig {
    return HttpClientsConfig(
      endpoints = mapOf(
        "robots" to HttpClientEndpointConfig(jetty.httpServerUrl.toString())
      )
    )
  }
}

@MiskTest(startService = true)
class RobotLocatorIntegrationTest {
  @Suppress("unused")
  @MiskTestModule val module = MyServerModule()

  @Inject lateinit var jettyService: JettyService

  @Test 
  fun `makes a call to the service`() {
    val robotLocator = Guice.createInjector(MyClientModule(jettyService))
      .getInstance<RobotLocator>()

    robotLocator.SayHello().executeBlocking(HelloRequest.builder().name("world").build()) shouldBe 
      HelloReply.Builder().message("hello world").build()
  }
}