Tuesday, October 29, 2024

Create a zip and return it as response from SpringBoot WebFlux reactive API - Kotlin

SpringBoot WebFlux provides the framework where we can create reactive APIs.

What if we need to return a zip file as a response to such an API? lets go through the steps that require to send a zip file as a response from a reactive REST API

1. Create Route

We first need to create a GET route which can be used to request for a zip file.

@Configuration(proxyBeanMethods = false)
class RouterConfiguration {
    @Bean
    fun route(): RouterFunction<ServerResponse> {
        return RouterFunctions
            .route(GET("/download").and(accept(MediaType.APPLICATION_OCTET_STREAM))) { ServerResponse.ok().build() }
    }
}

this configures a GET route called /download which match to the request only if it supports receiving octet stream, by declaring it in its request header.

Route upon invocation will just return 200 OK status with an empty response body.

2. Create Controller Function

We now need an function that can build this zip file, so that can be called from the route invocation and returned as a response.

If we understand properly, zip is a series of bytes, which can represented by a Spring provided flux of DataBuffer.

Therefore it can be defined as this to return that flux of databuffers as a response body for a request.

@Component
class DownloadHandler() {
    fun createZip(request: ServerRequest): Mono<ServerResponse> {
        return ServerResponse.ok().contentType(MediaType.parseMediaType("application/zip"))
            .body(BodyInserters.fromDataBuffers(Flux.empty()))
    }
}

The method accepts a ServerRequest and builds ServerResponse for empty list of bytes with content-type as zip.

Above created controller method can now invoked from the route configuration for request handling. 

With above change, altered route configuration method will look as this.

@Bean
fun route(downloadHandler: DownloadHandler): RouterFunction<ServerResponse> {
  return RouterFunctions
    .route(GET("/download").and(accept(MediaType.APPLICATION_OCTET_STREAM))) { downloadHandler.createZip(it) }
}

3. Generate Zip Content

The last step that is remaining is building a zip file from list of files. 

Lets assume we have two files /one/first.txt and /two/second.txt, then the Zip content can be build and streamed as DataBuffer.

private fun createZipDataBuffer(dataBufferFactory: DataBufferFactory): Flux<DataBuffer> {
    val paths = listOf(Path("/one/first.txt"), Path("/two/second.txt"))
    // Create a data buffer of 5 kibibytes
    val dataBuffer = dataBufferFactory.allocateBuffer(1024 * 5) // 5 kibibyte
    // Create a zip output stream using the data buffer as the output stream
    val zipOutputStream = ZipOutputStream(dataBuffer.asOutputStream())

    return Flux.create { emitter ->
        try {
            zipOutputStream.use {
                paths.forEach { path ->
                    zipOutputStream.putNextEntry(ZipEntry(path.subpath(0, path.nameCount - 1).toString()))
                    Files.copy(path, zipOutputStream)
                    zipOutputStream.closeEntry()
                }
                zipOutputStream.finish()
                emitter.next(dataBuffer)
                emitter.complete()
            }
        } catch (e: Exception) {
            emitter.error(e)
        }
    }
}

Method requires DataBufferFactory to create the DataBuffer sized with 5 kibibyte. and the factory from the request should be used for it.

To represent a zip file,ZipOutputStream is created and the generated data buffer used as its output stream and upon completion, the generated data buffer emitted from the flux, so it can be used to create the response.

With that this method can now invoked from DonloadHandler createZip method like this.

fun createZip(request: ServerRequest): Mono<ServerResponse> {
    return ServerResponse.ok().contentType(MediaType.parseMediaType("application/zip"))
        .body(BodyInserters.fromDataBuffers(
	    // use the bufferFactory from the request
            createZipDataBuffer(request.exchange().response.bufferFactory()))
        )
}

With all that, when a GET request is invoked to the /download url, then a zip file, with both files, will be send as a response.

That's all and now it should be possible to create a zip content as a response for a reactive SpringBoot WebFlux API 👌

Saturday, August 31, 2024

What really is the `Connection prematurely closed BEFORE response` exception from Netty HttpClient?

Have you tried calling a HTTP api using Netty client? probably you would have indirectly using it if you are using the SpringBoot webclient to make such API calls, cause the WebClient internally uses netty's HttpClient for making the calls in non blocking way.

The client designed to keep the connection in a connection pool in order to reuse it for the next time, when a request is initiated for the same server. However, rarely this client might throw reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response exception.

This often probably means, that the connection is closed by the destination server, while a request sending is initiated using this connection by the netty's HttpClient.

Suppose the connection is closed by the destination server, while the connection is idle in the netty's connection pool, then this exception will not occur if the destination server property send its signal to close the connection. In such case netty will disregard that idle connection and will initiate new one when next time new request comes for the same destination server.

Therefore, this exception is difficult to recreate and occur rarely.

Possible scenario

Following is one possible scenario that this exception can be observed occasionally.

Assume you have a Http API deployed using the tomcat based spring boot server and you started to make API call using netty's HttpClient for every 60 seconds.

Then rarely you might observe few PrematureCloseException over time. This is because the tomcat, by default, keeps the connection idle for 60 seconds and when while netty try to use that connection, if tomcat mark it for close, due to the 60 seconds timeout, then call fails with PrematureCloseException.

Above mentioned problem can be fixed if the tomcat connection idle timeout set to more than 60 seconds or netty's http connection idle timeout set to less than 60 seconds.

How to set connection idle timeout for Tomcat

Mostly keepAliveTimeout setting, which set to the value of connectionTimeout by default, mistakenly taken as connection idle timeout setting to be adjusted. However, it has nothing to do with keeping the connection idle after serving a request.

keepAliveTimeout used to wait for another read from the same request, before concluding it as completed.

Once response sent, and client completes the request, then the tomcat connection left with OPEN_READ status, which means it is ready to start receiving new request, in that state, it uses connectionTimeout to decided whether it should idle the connection further or close the connection.

Therefore tomcat connectionTimeout is the one that should be changed, if you want to change how long a connection should be kept idle.

Also note that, if the connection is reused by tomcat(if got a new request from same client within connectionTimeout), then if a TLS handshake is already performed, it will be skipped next time removing the overhead of TLS handshake.

How to set connection idle timeout for Netty client

From netty's pool perspective, connection can be set to expire after being idle using the property maxIdleTimeout.


With that, it should be possible to handle the PrematureCloseException from netty's HttpClient.


Sunday, July 7, 2024

Gitlab pipeline to build and publish a helm chart to artifactory

Lets see how we can use gitlab pipeline to build a helm chart and publish it to artifactory using its API.

Before that it is good to have some basic understanding on gitlab repository and its pipeline before following this guide.

Identify Required Stages and Jobs

Gitlab pipeline executes the stages in defined order, a stage may contain one or more jobs. Job in a stage will not start until the previous stage is completed.

For the purpose of build and publish the helm chart, we can think about following jobs build, render, lint, package and publish that can be grouped into following 3 stages build, test and release, which has to be executed in the defined order.

Lets define the stages in the pipeline file .gitlab-ci.yml

stages:
  - build
  - test
  - release

Docker image for job

Gitlab jobs are executed in a docker image by a runner. In our case we want to execute helm commands, therefore it is better to run the jobs in a docker container where the helm command is supported.

Lets use alpine/helm:3.13.3 docker image for this purpose. Since this is going to be used by almost all the jobs, it can be defined in global level in the pipeline like this.

default:
  image:
    name: alpine/helm:3.13.3
    entrypoint: [""]

Note that the default entrypoint of the image is overridden to value empty to get the access to the shell when executing the scripts in the job

Build helm chart

First thing we should do is the helm dependency build to make sure that helm chart dependency are valid and reachable.

Lets define the build job for it.

build:
  stage: build
  script:
    - helm dep build

Note down the stage of the job which is assigned to the stage build. Upon a trigger of the pipeline, given script, helm dep build will be executed by the gitlab pipeline.

Test helm chart

Helm chart can be tested by running helm template and helm lint. These commands will make sure that our rendered chart content is valid as per the mock values given to it.

These two verification can be performed in the test stage that will be executed once build stage is completed.

For the both helm template and lint commands, we can use a same value file, which would contain the mock value that required to render the helm chart. Therefore, this sample file can be declared as global variable.

variables:
  LINT_VALUE_FILE: values-lint.yaml

Both jobs can be defined as this

render:
  stage: test
  script:
    - helm template . -f $LINT_VALUE_FILE > rendered.yaml

lint:
  stage: test
  script:
    - helm lint -f $LINT_VALUE_FILE

make sure you have a file named values-lint.yaml created in your chart's root folder for the commands to get success.

Package chart

Helm chart should be packaged first before it pushed into artifactory. Fortunately helm has a command package for this purpose.

One thing to note is, whenever we run the pipeline from master branch, we shall package with a snapshot version, and when run from TAG then we shall package with concreate chart version to support continues integration of latest changes.

For the snapshot we can toss up a version suffix like this -dev.$CI_JOB_ID+$CI_COMMIT_SHORT_SHA where the gitlab inbuilt variable $CI_JOB_ID is the current job id and the $CI_COMMIT_SHORT_SHA is the latest short commit SHA of the branch/TAG.

Job for the package can be declared like this.

package:
  stage: release
  before_script:
    - CURRENT_CHART_VERSION=$(helm show chart . | grep ^version | cut -d ':' -f 2 | cut -d ' ' -f 2)
  script:
    - PACKAGE_VERSION=$CURRENT_CHART_VERSION$VERSION_SUFFIX
    - echo "Version to create the package is $PACKAGE_VERSION"
    - helm package . --version $PACKAGE_VERSION -d build/
  rules:
    - if: $CI_COMMIT_BRANCH == "master"
      variables:
        VERSION_SUFFIX: -dev.$CI_JOB_ID+$CI_COMMIT_SHORT_SHA
    - if: $CI_COMMIT_TAG
      variables:
        VERSION_SUFFIX: ""
  artifacts:
    paths:
      - build/*.tgz
    expire_in: 1 day

Few things to notice in this job declarations are

  • Chart version extracted from the root Chart.yaml version with the help of helm show chart result, where the version value is extracted using the cut tool.
  • Version suffix is appended when the pipeline run in master branch and suffix omitted when it is run from a TAG.
  • Result of the package command attached as a job artifact, so the next job can use this artifact to publish it to the artifactory.

Publish chart

Lets assume there is a artifactory deployed in the following address artifacts.devops.xyz.com and it contains two repositories hlm-tmp and hlm-prd for the purpose of snapshot artifacts and production artifacts.

Lets define this as global variables

variables:
  HELM_TMP_REPOSITORY: hlm-tmp
  HELM_PRD_REPOSITORY: hlm-prd
  ARTIFACTORY_HOST: artifacts.devops.xyz.com

It is a good idea to keep snapshot artifact(build from master) in one location and production artifact(build from TAG) in another location to maintain them in a better way.

Artifactory has REST API which can be used to push artifacts into repositories, which we can also use from a gitlab pipeline, with the help of tool like curl.

With that, a job to push a artifact to artifactory would look like this.

publish:
  stage: release
  image: 
    name: alpine/curl:8.8.0
    entrypoint: [""]
  needs:
    - package
  before_script:
    - HELM_PACKAGE_NAME=$(ls build/)
  script:
    - echo "Helm package to push $HELM_PACKAGE_NAME"
    - curl -u $ARTIFACTORY_USERNAME:$ARTIFACTORY_PASSWORD -X PUT "https://$ARTIFACTORY_HOST/$REPOSITORY/$HELM_PACKAGE_NAME" -T build/$HELM_PACKAGE_NAME
  rules:
    - if: $CI_COMMIT_BRANCH == "master"
      variables:
        REPOSITORY: $HELM_TMP_REPOSITORY
    - if: $CI_COMMIT_TAG
      variables:
        REPOSITORY: $HELM_PRD_REPOSITORY

Few things to notice in this job description are

  • Default image is overridden to use alpine/curl for the purpose of using the tool curl.
  • Job declared as depends on the job package through keyword needs to access the job artifact (helm package generated) from the job package.
  • Repository to push is decided based on where the pipeline is being executed. If it is in master, then temporary repository used. If it is in TAG then production repository is used.
  • Variables ARTIFACTORY_USERNAME and ARTIFACTORY_PASSWORD are expected to have username and password configured to access the artifactory API

That's pretty much all what we needed to build, test, package and publish a helm chart to artifactory using a gitlab pipeline.

To sum it up, complete pipeline configuration would look like this after each variable and job declarations.

default:
  image:
    name: alpine/helm:3.13.3
    entrypoint: [""]

variables:
  HELM_TMP_REPOSITORY: hlm-tmp
  HELM_PRD_REPOSITORY: hlm-prd
  ARTIFACTORY_HOST: artifacts.devops.xyz.com
  LINT_VALUE_FILE: values-lint.yaml

stages:
  - build
  - test
  - release

build:
  stage: build
  script:
    - helm dep build

render:
  stage: test
  script:
    - helm template . -f $LINT_VALUE_FILE > rendered.yaml

lint:
  stage: test
  script:
    - helm lint -f $LINT_VALUE_FILE

package:
  stage: release
  before_script:
    - CURRENT_CHART_VERSION=$(helm show chart . | grep ^version | cut -d ':' -f 2 | cut -d ' ' -f 2)
  script:
    - PACKAGE_VERSION=$CURRENT_CHART_VERSION$VERSION_SUFFIX
    - echo "Version to create the package is $PACKAGE_VERSION"
    - helm package . --version $PACKAGE_VERSION -d build/
  rules:
    - if: $CI_COMMIT_BRANCH == "master"
      variables:
        VERSION_SUFFIX: -dev.$CI_JOB_ID+$CI_COMMIT_SHORT_SHA
    - if: $CI_COMMIT_TAG
      variables:
        VERSION_SUFFIX: ""
  artifacts:
    paths:
      - build/*.tgz
    expire_in: 1 day

publish:
  stage: release
  image: 
    name: alpine/curl:8.8.0
    entrypoint: [""]
  needs:
    - package
  before_script:
    - HELM_PACKAGE_NAME=$(ls build/)
  script:
    - echo "Helm package to push $HELM_PACKAGE_NAME"
    - curl -u $ARTIFACTORY_USERNAME:$ARTIFACTORY_PASSWORD -X PUT "https://$ARTIFACTORY_HOST/$REPOSITORY/$HELM_PACKAGE_NAME" -T build/$HELM_PACKAGE_NAME
  rules:
    - if: $CI_COMMIT_BRANCH == "master"
      variables:
        REPOSITORY: $HELM_TMP_REPOSITORY
    - if: $CI_COMMIT_TAG
      variables:
        REPOSITORY: $HELM_PRD_REPOSITORY

Monday, June 24, 2024

Java SSL exceptions are not really the end of the world

Have you ever felt like an end of the world when you tried to make a HTTP call and end up with an SSL exception in java? then you are probably not alone. 

For most of us the java SSL errors are vague and leave us with no option on how to fix that. Its because in the stacktrace, usually there will not be any specific message, except mostly the PKIX path building failed message, which points to the exact problem, where the fix might be needed.  

But there is nothing to worry about it. If you don't know your way around this exception, all you need to know about is the system property javax.net.debug.

Setting the system property as javax.net.debug=all would print all the certificate exchanges and SSL handshake messages that happens between client and server to the log.

Using that log it should be relatively easy to understand what causes the SSL connection failure.

Since it's a system property it can be set as a jvm argument when starting up the java program in a similar way like java -Djavax.net.debug=all

This property certainly make the debugging and understanding of the SSL errors better.

In case if you wanted to know more about this property then check out the guide Debugging SSL/TLS Connections in javase docs.