1.6.0 release notes

7th April 2021

🌟 New features

  • You can now send and handle multipart requests and responses. #253 #3327

    // Send a multipart message.
    WebClient client = WebClient.of("http://example.com/");
    BodyPart part1 = BodyPart.of(ContentDisposition.of("form-data", "username"), "Armeria");
    BodyPart part2 = BodyPart.of(ContentDisposition.of("form-data", "password"), "mypassword");
    Multipart multipart = Multipart.of(part1, part2);
    client.execute(multipart.toHttpRequest("/login"));
    
    // Handle a multipart message.
    Server.builder()
          .service("/login", (ctx, req) -> {
              Multipart multipart = Multipart.from(req);
              multipart.aggregate().thenApply(aggregated -> {
                  assert aggregated.field("username").contentUtf8().equals("Armeria");
                  assert aggregated.field("password").contentUtf8().equals("mypassword");
                  ...
              }));
         });
  • Armeria now provides various useful extensions and conversions for Scala. #3395

  • You can now create a StreamMessage from a Path or a File. #3344

    Path path = Paths.get("...");
    StreamMessage<HttpData> publisher = StreamMessage.of(path);
  • You can now filter or transform values of a StreamMessage using StreamMessage.filter() or StreamMessage.map(). #3351

    // Filter.
    StreamMessage<Integer> source = StreamMessage.of(1, 2, 3, 4, 5);
    StreamMessage<Integer> even = source.filter(x -> x % 2 == 0);
    
    // Transform.
    StreamMessage<Integer> source = StreamMessage.of(1, 2, 3, 4, 5);
    StreamMessage<Boolean> isEven = source.map(x -> x % 2 == 0);
  • You can now send a different response depending on the exception. #3209 #3413

    Server.builder().exceptionHandler((ctx, cause) -> {
        if (cause instanceof RequestTimeoutException) {
            // The request timed out!
            return AggregatedHttpResponse.of(...);
        }
    
        // Return null to let ExceptionHandler.ofDefault() convert the exception.
        return null;
    })...
  • You can now convert an exception into an RpcResponse in THttpService. #3349 #3383

    THttpService.builder()
                .addService("hello", helloService)
                .exceptionHandler((ctx, cause) -> {
                    if (cause instanceof IllegalArgumentException) {
                        return RpcResponse.of(new CustomizedException("Bad Request!"));
                    }
                    ...
                })
  • You can now set a response timeout and attributes using WebClientRequestPreparation. #3347 #3357

    WebClient client = ...;
    client.prepare()
          .get("/my-service")
          .responseTimeout(Duration.ofSeconds(3))
          .attr(USER_ID, userId)
          .attr(USER_SECRET, secret)
          .execute();
  • You can now specify additional error details to a gRPC response when an exception is raised in a gRPC service. #3307 #3329

    GrpcService.builder()
               .exceptionMapping((cause, metadata) -> {
                   if (throwable instanceof AuthError) {
                       metadata.put(KEY, toMetadata(cause))
                       return Status.UNAUTHENTICATED.withCause(cause);
                   }
                   ...
               })
  • You can now customize the success condition of a metric. #3404 #3410

    MetricCollectingService.builder(...)
                           .successFunction((context, log) -> {
                                final int statusCode = log.responseHeaders().status().code();
                                return (statusCode >= 200 && statusCode < 400) || statusCode == 404;
                            });
  • You can now fluently build a DecodingClient using the DecodingClientBuilder. #3348 #3372

    DecodingClient.builder()
                  .autoFillAcceptEncoding(false)
                  .strictContentEncoding(true)
                  .newDecorator();
  • You can now fluently build an HttpRequest with a Publisher<HttpData>. #3343

    StreamMessage<HttpData> publisher = StreamMessage.of(...);
    HttpRequest.builder()
               .method(HttpMethod.GET)
               .path("/")
               .content(MediaType.PLAIN_TEXT_UTF_8, publisher)
               .build();
  • You can now convert a stream of Protobuf Messages into JSON Text sequences using an annotated service. #3394

    @Get("/items")
    @ProducesJsonSequences
    public Publisher<MyProtobufMessage> protobufJsonSeqPublisher() {
        return StreamMessage.of(MyProtobufMessage.newBuilder()...build(),
                                MyProtobufMessage.newBuilder()...build());
    }
    
    @Get("/items")
    @ProducesJsonSequences
    public Stream<MyProtobufMessage> protobufJsonSeqPublisher() {
        return Stream.of(MyProtobufMessage.newBuilder()...build(),
                         MyProtobufMessage.newBuilder()...build());
    }
  • You can now customize the default service name of a RequestLog. #3232 #3366

    Server.builder()
          .defaultServiceNaming(ctx -> {
              final ServiceConfig config = ctx.config();
              return config.route().patternString();
         });
    // For a specific service.
    Server.builder()
          .route().path("/")
          .defaultServiceNaming(...)
          ...
  • You can now check if the current request is matched by any routes or not. #3365 #3378

    ServerBuilder sb = ...
    sb.decorator((delegate, ctx, req) -> {
        if (ctx.config().route().isFallback()) {
            // This request is not matched any routes.
        }
        return delegate.serve(ctx, req);
    });
  • BraveService does not trace requests for TransientService such as HealthCheckService anymore. #3382

    HealthCheckService.builder()
                      .transientServiceOptions(WITH_TRACING)
                      ...
                      .build();
  • You can now clean up resources by overriding FilteredStreamMessage.onCancellation() when Subscription.cancel() is called. #3375

    new MyFilteredHttpResponse(res) {
        ...
        @Override
        protected void onCancellation(Subscriber<? super U> subscriber) {
            // Clean up resources.
        }
    }
  • You can now easily set 'cookie' or 'set-cookie' headers. #3388 #3391

    Cookie cookie = Cookie.of("cookie", "value");
    RequestHeaders headers = RequestHeaders.builder(HttpMethod.GET, "/")
                                           .cookie(cookie)
                                           .build();
    assert headers.cookies().equals(Cookies.of(cookie));
    
    Cookie setCookie1 = ...
    Cookie setCookie2 = ...
    ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK)
                                             .cookies(setCookie1, setCookie2)
                                             .build();
    assert headers.cookies().equals(Cookies.of(setCookie1, setCookie2));
  • You can now use req.root_id BuiltInProperty to log the ID of RequestContext.root(). #3429 #3433

  • You can now use Thrift 0.14.0 with the new armeria-thrift0.14 module. #3470 #3422

  • You can now use OAuth 2.0 related features with the new armeria-oauth2 module. #2268 #2840

  • You can now use Jakarta RESTful Web Services on top of Armeria with the armeria-resteasy module. #3285 #3296

📈 Improvements

🛠️ Bug fixes

  • You no longer see NullPointerException in HttpResponseDecoder. #3036 #3407
  • You no longer see 405 Method Not Allowed when the exact and param path are defined with different HTTP methods. #3330 #3340
  • You no longer see Address family not supported by protocol or Connection refused error anymore on certain machines with IPv6 enabled. #3425
  • The Unicode characters like emojis in a JSON response are now rendered correctly in DocService. #3396
  • You no longer see the wrong response body when the payload violates the protocol or is too large. #3419
  • You can now use the Thrift client and service that is generated by java:fullcamel option of Thrift compiler. #3269 #3360 #3369
  • SmartLifecycle for Armeria server graceful shutdown is only created when the Armeria server is created by Spring integration. #3300
  • You can now use Sealed oneofs message from ScalaPB with JSON for gRPC and annotated services. #3342 #3394
  • You no longer see CancellationException when an HttpResponse is fully consumed on server-side. #3387
  • You no longer see ClosedSteamException in Jetty service when it fails to write to an HttpResponse. EofException is raised instead. #3412
  • You no longer see IllegalStateException when RequestContextHooks is enabled. #3441 #3442
  • ArmeriaServerHttpResponse of Spring Webflux integration now correctly propagates Reactor's Context. #3439 #3443

☢️ Breaking changes

⛓ Dependencies

  • Bucket4J 4.10.0 → 6.2.0
  • Curator 4.3.0 → 5.1.0
    • ZooKeeper 3.5.8 → 3.6.2
  • Dropwizard 2.0.18 → 2.0.20
  • Dropwizard Metrics 4.1.17 → 4.1.18
  • gRPC 1.35.0 → 1.36.1
  • Jackson 2.12.0 → 2.12.2
  • java-jwt 3.12.1 → 3.14.0
  • Jetty 9.4.36 → 9.4.39
  • Micrometer 1.6.3 → 1.6.5
  • Netty 4.1.58 → 4.1.63
    • Netty TCNative BoringSSL 2.0.36 → 2.0.38
    • Netty io_uring transport 0.0.3 → 0.0.5
  • OpenSAML 3.4.5 → 3.4.6
    • Shibboleth java-support 7.5.1 → 7.5.2
  • Reactor 3.4.2 → 3.4.4
    • Reactor Kotlin extensions 1.1.2 → 1.1.3
  • RxJava 3.0.9 → 3.0.11, 2.2.20 → 2.2.21
  • ScalaPb 0.10.10 → 0.11.0
  • Spring Boot 2.4.2 → 2.4.4
  • Tomcat 9.0.41 → 9.0.44, 8.5.61 → 8.5.64

🙇 Thank you