Skip to content

Fix global logger setting interfering with response deflation parsing #816

@bkuhlmann

Description

@bkuhlmann

Why

Hello. 👋

I discovered a rather hard to debug situation when using HTTP's global configuration to configure logging and then ending up with a JSON parsing error due to an empty string in the HTTP response due compression (i.e. making an HTTP request where the content is in gzip format).

How

Here's how to recreate the issue quickly using the following script:

#! /usr/bin/env ruby
# frozen_string_literal: true

# Save as `demo`, then `chmod 755 demo`, and run as `./demo`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "debug"
  gem "http"
  gem "json"
  gem "logger"
end

uri = "https://alchemists.io/api/alfred/projects.json"
logger = Logger.new STDOUT

HTTP.default_options = HTTP::Options.new features: {logging: {logger:}}

HTTP.use(:auto_inflate)
    .get("https://alchemists.io/api/alfred/projects.json")
    .then { it.parse }
    .then { puts it }

The above will result in the following error:

json-2.15.2/lib/json/common.rb:358:in 'JSON::Ext::Parser.parse': unexpected end of input at line 1 column 1 (JSON::ParserError)

The problem is that the global logger configuration ends up parsing the body first (roughly speaking) before the HTTP response can be parsed. This causes the above error because by the time you ask the HTTP response to parse itself, you only have an empty string at that point.

The workaround to the above is to not define the logger as a global default but use the logger in each request. For example, the following will work:

HTTP.use(:auto_inflate)
    .use(logging: {logger: logger})
    .get(uri)
    .then { it.parse }
    .then { puts it }

Would it be possible to ensure setting a global logger doesn't interfere with the final parsing of the result?

Notes

By the way, if setting the logger per each request is the only way to handle this, is there harm in using a single HTTP instance throughout your entire application? Example:

CLIENT = HTTP.use(:auto_inflate).use(logging: {logger: logger})

This way you could semi-provide a global configuration for HTTP that works. The only problem is that I haven't tested this over extensive use to see if there memory leaks or concurrency issues using HTTP like this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions