diff --git a/guides/handling-errors.md b/guides/handling-errors.md index 9833a915..b7e3c9da 100644 --- a/guides/handling-errors.md +++ b/guides/handling-errors.md @@ -121,6 +121,30 @@ The list could also be partial populated and truncated because of a timeout. <% end %> ``` +## HTTP errors + +By default, `GraphQL::Client::HTTP` turns HTTP statuses other than 200 and 400 +into a GraphQL-style error, so a `503` shows up in `response.errors` like any +other error. Override `parse_response` to handle the HTTP response yourself, +for example to raise your own errors: + +```ruby +HTTP = GraphQL::Client::HTTP.new("https://example.com/graphql") do + def parse_response(response) + case response + when Net::HTTPOK, Net::HTTPBadRequest + super + else + raise MyApp::UpstreamError, "HTTP #{response.code} #{response.message}" + end + end +end +``` + +`parse_response` receives the `Net::HTTPResponse` with status, headers and body +and returns the GraphQL result hash. Network and timeout errors are raised by +`Net::HTTP` before it is called. + ## See also * [graphql-js "path" field](https://github.com/graphql/graphql-js/blob/23592ad16868e06b1c003629759f905a77ab81a0/src/error/GraphQLError.js#L42-L48) diff --git a/lib/graphql/client/http.rb b/lib/graphql/client/http.rb index c3e3fb87..8d17e9a9 100644 --- a/lib/graphql/client/http.rb +++ b/lib/graphql/client/http.rb @@ -77,6 +77,16 @@ def execute(document:, operation_name: nil, variables: {}, context: {}) response = connection.request(request) @last_response = response.to_hash + parse_response(response) + end + + # Public: Extension point for subclasses to turn the HTTP response into a + # GraphQL result, e.g. to raise on unexpected statuses. + # + # response - The Net::HTTPResponse + # + # Returns { "data" => ... , "errors" => ... } Hash. + def parse_response(response) case response when Net::HTTPOK, Net::HTTPBadRequest JSON.parse(response.body) diff --git a/test/test_http.rb b/test/test_http.rb index 4c7deb68..ef5955c4 100644 --- a/test/test_http.rb +++ b/test/test_http.rb @@ -2,6 +2,7 @@ require "graphql" require "graphql/client/http" require "minitest/autorun" +require "minitest/mock" class TestHTTP < Minitest::Test SWAPI = GraphQL::Client::HTTP.new("https://mpjk0plp9.lp.gql.zone/graphql") do @@ -35,3 +36,48 @@ def test_execute assert_equal(expected, actual) end end + +class TestHTTPParseResponse < Minitest::Test + class UnexpectedStatus < StandardError; end + + def http_response(status, body) + response = Net::HTTPResponse::CODE_TO_OBJ.fetch(status).new("1.1", status, "Test response") + response.instance_variable_set(:@read, true) + response.body = body + response + end + + def execute(adapter, response) + connection = Minitest::Mock.new + connection.expect(:request, response, [Net::HTTP::Post]) + result = adapter.stub(:connection, connection) do + adapter.execute(document: GraphQL.parse("{ name }")) + end + connection.verify + result + end + + def test_default_response_handling + adapter = GraphQL::Client::HTTP.new("https://example.com/graphql") + + assert_equal({ "data" => { "name" => "Luke" } }, execute(adapter, http_response("200", '{"data":{"name":"Luke"}}'))) + assert_equal({ "errors" => [{ "message" => "Rejected" }] }, execute(adapter, http_response("400", '{"errors":[{"message":"Rejected"}]}'))) + assert_equal({ "errors" => [{ "message" => "503 Test response" }] }, execute(adapter, http_response("503", "unavailable"))) + end + + def test_parse_response_can_be_overridden + adapter = GraphQL::Client::HTTP.new("https://example.com/graphql") do + def parse_response(response) + raise UnexpectedStatus, response.code unless response.is_a?(Net::HTTPOK) + + super + end + end + response = http_response("503", "Unavailable") + + error = assert_raises(UnexpectedStatus) { execute(adapter, response) } + assert_equal("503", error.message) + assert_equal(response.to_hash, adapter.last_response) + assert_equal({ "data" => { "name" => "Luke" } }, execute(adapter, http_response("200", '{"data":{"name":"Luke"}}'))) + end +end