Redirecting requests using HTTP code in NGINX

Marcos
Javarevisited
Published in
4 min readFeb 21, 2023

Hi, my name is Marcos and today I have a interesting topic: supposing you have two Spring Boot (or whatever framework) based applications and want to balance the requests between them. In this text we will use Nginx and docker to make a redirect based on HTTP status code.

What we must have installed previously?

  • Docker
  • Maven
  • jdk 17

What we will do?

  • Create a docker-compose file that runs Docker, .jar applications and Nginx;
  • Create a rule to redirect when a http error 418 happen;
  • Use rewrite command to change the url;
  • Name a location

At first, we must put the two applications in same directory. Then, let’s create a new directory called “nginx”.

In this example we pretend to redirect the request when an error 418 happens. Using this, is possible to make a rollout feature in one service and redirect the requests to another url.

At first, create a file called nginx.conf:

server {
listen 9090;
location /route-0 {
proxy_intercept_errors on;
recursive_error_pages on;
rewrite ^/route-0$ /route-1; #change the url
error_page 418 = @test; #every time service1 retuns 308, this location will be called
proxy_pass http://service1:8080;
break;
}

location @test {
#more_set_input_headers 'X-Nginx-Forwarded: true';
#It is possible to rollout per client here and return 418
rewrite ^/route-1$ /route-2; #change the url
proxy_pass http://service2:8080;
break;
}
}

Here, I pretend to send 50% of requests to service1 and 50% for service2.

The service1 responds an HTTP 200 or a HTTP 418 and service2 responds an HTTP 200.

In order to implement the responses above, I did this code in the controller of service1:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/route-1")
public class HomeController {

@Value("${test}")
private String test;
private static Integer counter = 0;

@PostMapping
public ResponseEntity<?> test(@RequestBody Person person) {
counter++;
if(counter % 2 == 0){
log.info(person.toString());
log.info("service 1 executed and returning success");
return ResponseEntity.ok(person);
}
log.info("service 1 executed and returning i am a teapot ---");
return new ResponseEntity(HttpStatus.I_AM_A_TEAPOT);
}

}

And this code in the controller of service2:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/route-2")
public class HomeController {

@Value("${test}")
private String test;

@PostMapping
public ResponseEntity<?> test(@RequestBody Person person) {
log.info(person.toString());
log.info("service 2 executed and returning success");
return ResponseEntity.ok().header("service", "service-2").body(person);
}

}

Create a Dockerfile for nginx:

FROM nginx
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf

Now, get the two applications. In this example I created the service1 and service2 directories but you can put the name you want.

Organize the files in this structure:

/loadbalancer/
--/nginx/
----/Dockerfile
----/nginx.conf
--/service1/
----/src
----/pom.xml
--/service2
----/src/
----/pom.xml

Create a Dockerfile to build a docker image using a spring boot application:

FROM openjdk:17-jdk-slim-buster
MAINTAINER marcbarbosa
COPY /target/app1-0.0.1-SNAPSHOT.jar /app/app.jar
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Now, copy this Dockerfile inside service1 and service2, the structure will be this:

/loadbalancer/
--/nginx/
----/Dockerfile
----/nginx.conf
--/service1/
----/Dockerfile
----/src
----/pom.xml
--/service2
----/Dockerfile
----/src/
----/pom.xml

It’s time to create our docker-compose file:

version: '3'
services:
lb:
build:
context: nginx
dockerfile: Dockerfile
ports:
- "9090:9090"
networks:
- my-network
depends_on:
- service1
- service2
service1:
build:
context: service1
dockerfile: Dockerfile
ports:
- "8181:8080"
networks:
- my-network
service2:
build:
context: service2
dockerfile: Dockerfile
ports:
- "8282:8080"
networks:
- my-network
networks:
my-network:
driver: bridge

I will show the structure again:

/loadbalancer/
--/docker-compose.yml
--/nginx/
----/Dockerfile
----/nginx.conf
--/service1/
----/Dockerfile
----/src
----/pom.xml
--/service2
----/Dockerfile
----/src/
----/pom.xml

How about test?

The service1 and service2 responds with the same body sended, the difference is that the service2 has a header in response with “service-2” in its content.

In order to test we must generate the .jar files. For this, inside of the directory service1, run this command:

mvn clean install

Do the same in service2 directory.

Now, these two:

- docker-compose build
- docker-compose up -d

Now, use postman and import this curl:

curl --location --request POST 'http://localhost:9090/route-0' \
--header 'Content-Type: application/json' \
--data-raw '{
"age":100,
"name": "Teste"
}'

The result is:

One request processed by service1:

And another by service2:

It’s also possible to see this behavior looking at the logs of docker:

If you want to access the applications directly you can do this using http://localhost:8181 and http://localhost:8282

That’s it for today.

--

--

Marcos
Javarevisited

I study software development and I love memes.