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 detailsHttpCall- 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()
}
}