graphql-client icon indicating copy to clipboard operation
graphql-client copied to clipboard

Dynamically set headers when calling `Client#parse`

Open wandy-dev opened this issue 6 years ago • 8 comments

This is related to https://github.com/github/graphql-client/issues/192 but I think it warrants it's own issue.

Our api has the schema behind authorization as well. So this example for setting up a query in the readme would also require context as an argument.

HeroNameQuery = SWAPI::Client.parse <<-'GRAPHQL'
  query {
    hero {
      name
    }
  }
GRAPHQL

But if we look at the source code for the parse method we can see that there is no such option. https://github.com/github/graphql-client/blob/b9cbc7229798e2323dde2c732172a9cd5b691b83/lib/graphql/client.rb#L105

Is there any way to pass headers when defining queries? It seems to me that either every method implemented by lib/graphql/client.rb needs to have the context argument, or there needs to be a way to set it dynamically when the class gets initialized.

If there isn't currently a way to pass headers through the parse method, I'm going to have to write a wrapper that functions a bit differently from the one in the readme.

wandy-dev avatar Jun 20 '19 18:06 wandy-dev

You can set up your client to accept headers like this

HTTP = GraphQL::Client::HTTP.new(url) do
  def headers(context)
    token = context[:access_token]
    {
      "Authorization": "token #{token}",
      "Accept-Encoding": "gzip"
    }
  end
end
SCHEMA = GraphQL::Client.load_schema(HTTP)
CLIENT = GraphQL::Client.new(schema: SCHEMA, execute: HTTP)

We do this for all our authenticated APIs and it works great. Let me know if that doesn't answer your question though, I might be misunderstanding.

ericyd avatar Aug 01 '19 16:08 ericyd

Of course, I just saw an identical answer on #192 which you referenced 😂 sorry about that. I don't think I'm understanding the issue

ericyd avatar Aug 01 '19 16:08 ericyd

Yeah this is definitely a weird one. I think it's related to how the ruby object model loads classes/modules, but I'm not sure about that.

I'll throw up a server with steps to reproduce. It seems like a simple config memoization will do the trick.

Thanks for getting back to me on this one.

wandy-dev avatar Aug 05 '19 21:08 wandy-dev

@ericyd So maybe "throwing up a server" was a little ambitious on my part. So I'll just copy the errors I'm getting.

[1] pry(main)> require "graphql/client"
=> false
[2] pry(main)> require "graphql/client/http"
=> true
[3] pry(main)> module GROUNDFLOOR_GRAPHQL
[3] pry(main)*   API_URL = "#{ENV['GF_API_URL']}graphql_api/v1"
[3] pry(main)*
[3] pry(main)*   HTTP = GraphQL::Client::HTTP.new(API_URL) do
[3] pry(main)*     def headers(context)
[3] pry(main)*       jwt = context[:jwt]
[3] pry(main)*       {
[3] pry(main)*       "Authorization": "Bearer #{jwt}",
[3] pry(main)*       'Content-Type': 'application/vnd.api+json',
[3] pry(main)*       'Accept': 'application/vnd.api+json'
[3] pry(main)*       }
[3] pry(main)*     end
[3] pry(main)*   end
[3] pry(main)*   SCHEMA = GraphQL::Client.load_schema(HTTP)
[3] pry(main)*   CLIENT = GraphQL::Client.new(schema: SCHEMA, execute: HTTP)
[3] pry(main)* end
D, [2019-09-19T10:20:00.176360 #14035] DEBUG -- : [httplog] Connecting: localhost:5000
D, [2019-09-19T10:20:00.183468 #14035] DEBUG -- : [httplog] Sending: POST http://localhost:5000/graphql_api/v1
D, [2019-09-19T10:20:00.183544 #14035] DEBUG -- : [httplog] Header: accept-encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
D, [2019-09-19T10:20:00.183597 #14035] DEBUG -- : [httplog] Header: accept: application/json
D, [2019-09-19T10:20:00.183651 #14035] DEBUG -- : [httplog] Header: user-agent: Ruby
D, [2019-09-19T10:20:00.183707 #14035] DEBUG -- : [httplog] Header: content-type: application/json
D, [2019-09-19T10:20:00.183764 #14035] DEBUG -- : [httplog] Header: authorization: Bearer
D, [2019-09-19T10:20:00.183820 #14035] DEBUG -- : [httplog] Header: content-type: application/vnd.api+json
D, [2019-09-19T10:20:00.183913 #14035] DEBUG -- : [httplog] Header: accept: application/vnd.api+json
D, [2019-09-19T10:20:00.183968 #14035] DEBUG -- : [httplog] Header: connection: close
D, [2019-09-19T10:20:00.184021 #14035] DEBUG -- : [httplog] Header: host: localhost:5000
D, [2019-09-19T10:20:00.184071 #14035] DEBUG -- : [httplog] Header: content-length: 1451
D, [2019-09-19T10:20:00.184155 #14035] DEBUG -- : [httplog] Data: {"query":"query IntrospectionQuery {\n  __schema {\n    queryType {\n      name\n    }\n    mutationType {\n      name\n    }\n    subscriptionType {\n      name\n    }\n    types {\n      ...FullType\n    }\n    directives {\n      name\n      description\n      locations\n      args {\n        ...InputValue\n      }\n    }\n  }\n}\n\nfragment FullType on __Type {\n  kind\n  name\n  description\n  fields(includeDeprecated: true) {\n    name\n    description\n    args {\n      ...InputValue\n    }\n    type {\n      ...TypeRef\n    }\n    isDeprecated\n    deprecationReason\n  }\n  inputFields {\n    ...InputValue\n  }\n  interfaces {\n    ...TypeRef\n  }\n  enumValues(includeDeprecated: true) {\n    name\n    description\n    isDeprecated\n    deprecationReason\n  }\n  possibleTypes {\n    ...TypeRef\n  }\n}\n\nfragment InputValue on __InputValue {\n  name\n  description\n  type {\n    ...TypeRef\n  }\n  defaultValue\n}\n\nfragment TypeRef on __Type {\n  kind\n  name\n
ofType {\n    kind\n    name\n    ofType {\n      kind\n      name\n      ofType {\n        kind\n        name\n        ofType {\n          kind\n          name\n          ofType {\n            kind\n            name\n            ofType {\n              kind\n
           name\n              ofType {\n                kind\n                name\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}","operationName":"IntrospectionQuery"}
D, [2019-09-19T10:20:00.184232 #14035] DEBUG -- : [httplog] Status: 401
D, [2019-09-19T10:20:00.184311 #14035] DEBUG -- : [httplog] Benchmark: 0.00584 seconds
D, [2019-09-19T10:20:00.184370 #14035] DEBUG -- : [httplog] Header: content-type: application/vnd.api+json
D, [2019-09-19T10:20:00.184424 #14035] DEBUG -- : [httplog] Header: access-control-allow-origin: http://localhost:3000
D, [2019-09-19T10:20:00.184477 #14035] DEBUG -- : [httplog] Header: connection: close
D, [2019-09-19T10:20:00.184529 #14035] DEBUG -- : [httplog] Header: content-length: 69
D, [2019-09-19T10:20:00.184616 #14035] DEBUG -- : [httplog] Response:
{"errors":[{"status":"401","detail":"Invalid authorization header"}]}
KeyError: key not found: "data"
from /home/andy/Repos/webdev/groundfloor_rails/vendor/bundle/ruby/2.4.0/gems/graphql-1.9.12/lib/graphql/schema/loader.rb:16:in `fetch'

In order to retrieve the schema, I need to be able to pass a JWT at the initialization of the client. Also when defining queries using the client. The initialization query can't be preformed unless a jwt is passed.

Is there any way to fix this, or should I perhaps look at another gem?

wandy-dev avatar Sep 19 '19 14:09 wandy-dev

Thanks for the STR @wandy-dev , that makes the issue more clear.

I'll be honest, I'm not really a contributor to this repo, the author solved a problem for me so I was trying to address some low hanging fruit in the issues as a thank you.

My gut instinct on how to solve this is to load a static schema rather than populating it with an introspection query, which is what the load_schema method will do if its passed an instance of GraphQL::Client::HTTP.

An example of how to do this is shown in the README under Configuration: https://github.com/github/graphql-client#configuration In short, you would change

SCHEMA = GraphQL::Client.load_schema(HTTP)

to

SCHEMA = GraphQL::Client.load_schema("path/to/schema.json")

I think your post demonstrates an interesting issue though and bears further investigation. I imagine there is a way to solve this but have not dived deep enough into the code to figure it out right now.

ericyd avatar Sep 20 '19 16:09 ericyd

Try:

GraphQL::Client.load_schema(GraphQL::Client.dump_schema(HTTP, nil, context: { access_token: token }))

msaspence avatar Oct 16 '19 13:10 msaspence

So obviously this is no longer a priority for me. I'm no longer at the organization that I needed to raise this issue for.

If you would like to close it that's fine by me.

wandy-dev avatar Mar 25 '21 20:03 wandy-dev

I recently ran into this issue too in a project. It would definitely be useful to be able to supply the context in the parse function. Ultimately, I solved this by using a generic all-purpose environment variable for retrieving the schema, and supply a different API key in the context for the Client.query but this isn't ideal.

  GITHUB_ACCESS_TOKEN = ENV['GITHUB_API_TOKEN']

  HTTP = GraphQL::Client::HTTP.new('https://api.github.com/graphql') do
    def headers(context)
      unless token = context[:api_key] || GITHUB_ACCESS_TOKEN
        fail "Missing GitHub access token"
      end
      {
        "Authorization" => "Bearer #{token}",
        "User-Agent" => 'Ruby'
      }
    end
  end
  Schema = GraphQL::Client.load_schema(HTTP)
  Client = GraphQL::Client.new(schema: Schema, execute: HTTP)

vincentezw avatar Sep 14 '22 13:09 vincentezw