Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use Context? #1138

Closed
Sam-Kruglov opened this issue Feb 27, 2025 · 5 comments
Closed

How to use Context? #1138

Sam-Kruglov opened this issue Feb 27, 2025 · 5 comments
Labels
for: stackoverflow A question that's better suited to stackoverflow status: invalid An issue that we don't feel is valid

Comments

@Sam-Kruglov
Copy link

Confused on the documentation. So, the docs talk about using io.micrometer.context.ThreadLocalAccessor but doesn't give any full examples, e.g. I don't understand what you mean by "register it manually on startup or by calling io.micrometer.context.ContextRegistry#getInstance()" - you mean server startup like in on context refresh or do you mean like on request startup, so I have to add some sort of interceptor to deal with it? There's a quick mention of @LocalContextValue as well as GraphQLContext that it can be injected into the method signature - that seems like what I actually need, not sure why even mention micrometer stuff?

My usecase is that I want to return a partial result: I paginate through items, at some point the items might become bad so I will include both the items and corresponding errors/extensions. If I throw an error, data is null, so I return normally but set context and then in an interceptor I check it and populate the errors/extensions.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Feb 27, 2025
@Sam-Kruglov
Copy link
Author

Current solution:
In the resolver I inject context: GraphQLContext, then I use context.put("stuff", stuff) during resolver, then inside of WebGraphQlInterceptor I can get it from response.executionInput.graphQLContext and edit final response via response.transform where I can add errors alongside data.

@bclozel
Copy link
Member

bclozel commented Feb 28, 2025

I think you're conflating two different concepts: context propagation and the GraphQL context.

Context propagation in Spring for GraphQL and the Context Propagation Library are about propagating context values from one context object to another: thread locals, the GraphQL Context, the Reactor Context. While traditional, "one thread per request", applications use thread locals for such things, this doesn't work as soon as you're opting in for a different execution model. Context propagation helps you to seamlessly use all of those together.
Registering a ThreadLocalAccessor needs to happen as early as possible with the static API or using the ServiceLoader mechanism (see an example here).

The GraphQL Context is a map that applications can use to store any relevant information for fetching data: security authorities, hints, etc.

It seems your problem is mostly related to partial data and nullability in the schema? Maybe you can share a small code snippet showing that part of your schema and the relevant @SchemaMapping methods?

@bclozel bclozel added the status: waiting-for-feedback We need additional information before we can continue label Feb 28, 2025
@Sam-Kruglov
Copy link
Author

Sam-Kruglov commented Feb 28, 2025

So ThreadLocalAccessor needs to register once on app startup, not once per request? I want to store some stuff in the request-only context, so the next request should not have any data I added.

I want to populate graphql context (or whatever context, I don't mind if I use an abstraction, maybe it's better for consistency) with errors so that right before returning data I can also populate errors in the response alongside it.

I found a good example that's very similar to what I want: https://stackoverflow.com/questions/71795252

I have a paginated endpoint (Connection) and as I generate new items I want to include errors/warnings saying that the list that is getting generated is going off the tracks but I still want to return the items to draw a graph in my UI. So the UI will show the graph and it will also show warnings/errors about it. Let me know if you still want the exact code, it'll take me some time to anonymize it...

My current solution (described in the last comment) works fine, I just declare a @QueryMapping method parameter GraphQLContext and then forward it around in all internal methods and eventually might call GraphQLContext#put in case there's something wrong with the data but I still want to show the data. Then outside of the resolver, in an interceptor, I populate the errors by checking out the context. To be more specific, I only store a single key in GraphQLContext under the name my-xx-errors - and that is a list of elements of MyErrorObject type. Every time I find some sort of issue with my data, I add an item in that list. And the interceptor then maps each MyErrorObject into a GraphqlError.

I don't like that I must forward GraphQLContext everywhere and pollute my method signatures with it, thread local would be nice, I wish I could just call some static method to populate the context instead. I tried debugging ContextRegistry#getInstance().get*() methods but couldn't find anything that would get me access to GraphQLContext. Let me know if you have an idea for that. I am using servlet stack for now.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Feb 28, 2025
@bclozel
Copy link
Member

bclozel commented Mar 3, 2025

So ThreadLocalAccessor needs to register once on app startup, not once per request? I want to store some stuff in the request-only context, so the next request should not have any data I added.

Again, ThreadLocalAccessor has a different purpose. If you'd like to pass data between data fetchers and data loaders the GraphQLContext is perfectly fine for that.

I want to populate graphql context (or whatever context, I don't mind if I use an abstraction, maybe it's better for consistency) with errors so that right before returning data I can also populate errors in the response alongside it.

I think this is the main problem. With GraphQL, data fetchers are supposed to return data for a particular field, or an error. But never both. Maybe this is a sign that the issue should be resolved in a different way with your schema and datafetchers.

I paginate through items, at some point the items might become bad so I will include both the items and corresponding errors/extensions.

I think the missing requirement is here. You have not provided more details about that, so I'll come up with an example here. Assuming a schema like (pagination is irrelevant here):

type Query {
    playerStatsById(id: ID!): PlayerStats
}

type PlayerStats {
    id: ID!
    name: String
    rank: Int
}

Let's assume that some stats entries are invalid when a player never played a single game. Here, the "rank" could be -1.
Here, the rank field is nullable, so if an error happens, the rest of the PlayerStats data will be sent to the client. If you provide a dedicated data fetcher for this field and error when the value is invalid.

@Controller
public class PlayerStatsController {

	Random random = new Random();

	@QueryMapping
	public PlayerStats playerStatsById(@Argument Long id) {
		return new PlayerStats(id, "player " + id, random.nextBoolean() ? random.nextInt(1,99) : -1);
	}

	@SchemaMapping
	public Integer rank(PlayerStats playerStats) {
		if (playerStats.rank() == -1) {
			throw new IllegalStateException("Player has never played");
		}
		return playerStats.rank();
	}

	public record PlayerStats(Long id, String name, Integer rank) {

	}
}

Here, you would get the default exception handling. But of course you can throw a custom exception and handle it with a custom @GraphQlExceptionHandler to customize the returned error.

{
  "errors": [
    {
      "message": "INTERNAL_ERROR for 682a996e-863d-ea2d-7b46-84dc01eab2a9",
      "locations": [
        {
          "line": 4,
          "column": 5
        }
      ],
      "path": [
        "playerStatsById",
        "rank"
      ],
      "extensions": {
        "classification": "INTERNAL_ERROR"
      }
    }
  ],
  "data": {
    "playerStatsById": {
      "name": "player 12",
      "rank": null
    }
  }
}

If you have other quesitons, can you raise those on StackOverflow with the "spring-graphql" tag? I think those are more generally useful there. Thanks!

@bclozel bclozel closed this as not planned Won't fix, can't repro, duplicate, stale Mar 3, 2025
@bclozel bclozel added status: invalid An issue that we don't feel is valid for: stackoverflow A question that's better suited to stackoverflow and removed status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged labels Mar 3, 2025
@Sam-Kruglov
Copy link
Author

Sam-Kruglov commented Mar 3, 2025

I didn't know you could do it like that, cool. I see you're saying that semantically I can't include both data and error about the same field, so I think what I need is a top-level extension about that data field instead and threat them as "warnings". I found an answer on SO which isn't documented and I'll be using that: ExtensionsBuilder https://stackoverflow.com/a/77183562/6166627. EDIT: actually, on the web, apollo graphql client doesn't support top-level extensions, so I'll just stick with my current approach of returning both errors and data for now, or as an alternative I just make my warnings as part of data.

I have some suggestions for spring-graphql, so I'll open more: #1140, #1141

Thanks for your time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
for: stackoverflow A question that's better suited to stackoverflow status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

3 participants