OxDEAD Unicornz

Have you ever seen so many?

Nginx: TLS Termination for RESTful Service Responding in JSON

This is quite common nowadays to have some TLS terminatin reverse proxy in front of your REST API. Nginx is being used frequently for this purpose.

The problem I faced and spent some time trying to resolve is that API backing Nginx responds in JSON, but if something is wrong with the request itself or Nginx can’t reach the backend Nginx returns error page in ‘text/html’.

Basically what I was trying to do is to make nginx respond ‘application/json’ on 502 errors, when no backends is reachable and on 400 errors, when someone tries to get plain HTTP response on 443 port. Other errors are expected to be reported by API itself.

502s were easy, you just add error_page 502 directive and location to server definition and got what you want.

1
2
3
4
5
6
7
error_page 502 @502_JSON;

location @502_JSON {
  internal;
  default_type application/json;
  return 502 '{ "status": "502", "error": "Bad gateway" }';
}

400s were a bit more trickier. Despite lot of folks on the Internet say this should work it does not:

1
2
3
4
5
6
7
error_page 400 @400_JSON;

location @400_JSON {
  internal;
  default_type application/json;
  return 400 '{ "status": "400", "error": "Bad request", "error_message": "The plain HTTP request was sent to HTTPS port" }';
}

you still going to get the built-in ‘text/html’ page. What you want is error_page 497

1
2
3
4
5
6
7
error_page 497 @400_JSON;

location @400_JSON {
  internal;
  default_type application/json;
  return 400 '{ "status": "400", "error": "Bad request", "error_message": "The plain HTTP request was sent to HTTPS port" }';
}

this works as expected. Full server definition for reference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server {
        listen 443 ssl;

        server_name localhost;
        ssl_certificate /etc/nginx/ssl/nginx.crt;
        ssl_certificate_key /etc/nginx/ssl/nginx.key;
        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers         HIGH:!aNULL:!MD5;

        error_page 497 @400_JSON;
        error_page 502 @502_JSON;

        location @400_JSON {
          internal;
          default_type application/json;
          return 400 '{ "status": "400", "error": "Bad request", "error_message": "The plain HTTP request was sent to HTTPS port" }';
        }

        location @502_JSON {
          internal;
          default_type application/json;
          return 502 '{ "status": "502", "error": "Bad gateway" }';
        }

        location / {
                proxy_pass http://localhost:8080;
        }
}