1. 4

    Do you think you could come up with an nginx config that responds with text/plain or application/json conditionally depending on the Accept-Encoding request headers?

    1. 9

      Something like this? (I haven’t tested it, though.)

      location /ip {
          if ($http_accept ~ "application/json") {
              default_type application/json;
              return 200 "{\"ip\":\"$remote_addr\"}";
          }
          default_type text/plain;
          return 200 $remote_addr;
      }
      

      Note that you want the Accept HTTP header, Accept-Encoding specifies mostly the compression used in transit.

      Hmm, and on closer look, the docs say that default_type is not valid in an if block, so if the above doesn’t work, maybe this will:

      location /ip {
          if ($http_accept ~ "application/json") {
              add_header Content-Type application/json;
              return 200 "{\"ip\":\"$remote_addr\"}";
          }
          default_type text/plain;
          return 200 $remote_addr;
      }
      

      One more edit.

      This turns out to be surprisingly tricky. The first version doesn’t work, for the reason described above. The second version sort of works, but for a JSON request, it returns the Content-Type header twice – for some reason, nginx insists on including the default_type even if you explicitly set the header with add_header. One workaround seems to be to install https://github.com/openresty/headers-more-nginx-module, and use more_set_headers, but that requires a rebuild of nginx. If someone has a properly working version with just plain nginx, I’d be interested, but it seems much easier to just define a separate location, like /ip.json.

      1. 3

        I realized this using custom error pages and named locations, like this:

        location ^~ /ip {
            error_page 463 = @ip;
            error_page 464 = @ip_json;
        
            if ($arg_json) {
                return 464;
            }
            if ($http_accept = 'application/json') {
                return 464;
            }
        
            return 463;
        }
        
        location @ip {
            default_type text/plain;
        
            return 200 $remote_addr;
        }
        
        location @ip_json {
            default_type application/json;
        
            return 200 "{ \"ip\": \"$remote_addr\" }";
        }
        
        1. 2

          Ah, of course, I should have thought of internal redirects. And the trick with using a custom error code to force an internal redirect is also quite neat, I didn’t know that one. Thanks!

          And I’m guessing it would be possible to simplify this by just directly using the default_type and return directive of @ip in the first location block; the default shouldn’t need an internal redirect.