router icon indicating copy to clipboard operation
router copied to clipboard

YAML-based response headers configuration (e.g., subgraph to client, router originated)

Open garypen opened this issue 2 years ago • 20 comments

Our current header propagation plugin is all about managing headers from client -> subgraph. Currently, we don't propagate headers from subgraph -> client, but there are users who require this functionality.

Problems we'd like to address in resolving this:

  • How to resolve conflicts between subgraph responses?
  • How to best represent in configuration yaml?

garypen avatar Jun 20 '22 08:06 garypen

The use case for subgraph to client headers/cookies is often found in authentication. For example, when implementing OAuth2's standard you must send back a "state" to stop CSRFs securely to the client.

Right now I am forwarding the Set-Cookie header from subgraph to router response, intercepting cookies and changing them into headers when they return to the subgraph.

My use case doesn't have any level of conflict between microservices because all of the authentication with cookies happens in one service. If there was a way to just make that work, including cookie pass through, that would be great.

Current Script

LockedThread avatar Jun 20 '22 15:06 LockedThread

In the HTTP RFC we can have multiple headers with the same name, that could be an option from the configuration if users want to keep every duplicated headers. We could have something like merge_policy: duplicate and merge_policy: merge and if we merge we just handle conflicts depending the order we provide the configuration. If we same header propagation from different subgraph then we apply it in the same order and then the last one win. I think we already have the same behavior currently from client->subgraph.

bnjjj avatar Jun 23 '22 04:06 bnjjj

This was marked as being split out of an api-1.0 issue, #980, but isn't marked with api-1.0. It probably should be...

garypen avatar Aug 25 '22 07:08 garypen

I've done some investigation of the problem and figured out how to preserve headers from subgraph responses to the execution response. The tricky bit now is how to expose configuration functionality so that users can specify which header should be preserved and from which subgraphs such headers should be preserved. A good starting point would be to implement just the simple named propagation mechanism from the existing request configuration model and only for subgraphs. We could then progressively extend this to include regex matching, insertion and removal if required and across all subgraphs if we find a useful use case.

The initial support would look like this:

headers:
  request:
    <all existing header configuration would be moved under request>
  response:
    subgraphs:
      accounts:
        - propagate:
          named: "x-user-id"
          default: "abc123"

We wouldn't provide support for rename initially. I'm not sure how useful that would be, but we could add it later if required.

garypen avatar Aug 31 '22 08:08 garypen

When can we expect the fix for this?

mohit61 avatar Sep 29 '22 07:09 mohit61

@garypen please share updates and plan for this fix.

mayank-rastogi avatar Jan 21 '23 10:01 mayank-rastogi

Since I know a lot of folks are following this issue, it's worth noting that we shipped documentation that exemplifies how to send headers from subgraphs to clients using Rhai script in a recent release. You can see that in our docs here.

abernix avatar Feb 08 '23 17:02 abernix

To be clear: that doesn't mean this issue is closed/resolved, since we do want to make it possible to do this with YAML in the future. I am going to slightly modify the title of this issue though, in order to reflect that!

abernix avatar Feb 08 '23 18:02 abernix

Possibly this is now a dup of #2444. #2444 is linked to in our docs.

BrynCooke avatar Feb 08 '23 18:02 BrynCooke

Ah, good eye. Since this has a substantial number of emotes on it already and the original post still indicated a desire to solve this with YAML, I'm going to close #2444 as a duplicate of this and still keep this one open.

To get a fresher show of hands of votes, please don't hesitate to 👍 this comment if you're still looking for YAML and the Rhai based solution isn't sufficient for you!

abernix avatar Feb 08 '23 20:02 abernix

We need to update the docs. Currently they point to #2444

BrynCooke avatar Feb 08 '23 21:02 BrynCooke

Thanks for adding the example to the docs, @abernix. I'm getting the following error at runtime, using it (on Router v1.10.2):

2023-02-09T01:17:37.570234Z apollo-router/src/plugins/rhai.rs:1099 ERROR [trace_id=4d4b71459c4707098c46e94f50018582] apollo_router::plugins::rhai: map_response callback failed: Runtime error (line 17, position 26)
in closure call

I'm not too familiar with Rhai yet, but it seems like response.headers is not an Object, and errors when a non-existent property is accessed.

BrennanRoberts avatar Feb 09 '23 01:02 BrennanRoberts

@BrennanRoberts The error message is telling you that your rhai code is failing at line 17, column 26. If you can't sort this yourself, you can paste a snippet and we may be able to help.

response.headers is a map of "headers". This is valid syntax for checking if a particular header is present in map:

      if response.headers["set-cookie"] != () {

() indicates that the "set-cookie" header is not present in response.headers. If you just try to access

response.headers["set-cookie"]

and it isn't present, then you will get an exception.

garypen avatar Feb 09 '23 08:02 garypen

@garypen, I'm using the recently posted script from Apollo's doc's, so I wanted to call out that it might have issues that others might run into as well.

BrennanRoberts avatar Feb 09 '23 16:02 BrennanRoberts

Ah. I'll take a look at that when I have a few minutes.

garypen avatar Feb 09 '23 16:02 garypen

Thanks for the callout. You might try the example in #2444, in the meantime.

abernix avatar Feb 09 '23 16:02 abernix

This variation worked for me for sending multiple cookies back from a single subgraph, thanks to #2219:

  let add_cookies_to_response = |response| {
    if response.context["set_cookie_headers"]?.len > 0 {
      response.headers["set-cookie"] = response.context["set_cookie_headers"];
    }
  };

  service.map_response(add_cookies_to_response);
}

fn subgraph_service(service, subgraph) {
    let store_cookies_from_subgraphs = |response| {
      if response.headers.values("set-cookie")?.len > 0 {
        if response.context["set_cookie_headers"] == () {
          response.context.set_cookie_headers = []
        }

        response.context.set_cookie_headers += response.headers.values("set-cookie");
      }
    };

    service.map_response(store_cookies_from_subgraphs);
}

BrennanRoberts avatar Feb 09 '23 16:02 BrennanRoberts

Our use case for header forwarding are cache headers. We forward certain cache tags that are used in Varnish and extra response status headers, so we can skip caching a response when there is an error.

While the rhai script is a solution, it requires more work to define what headers should be forwarded, and since we have multiple projects running the same setup, we also have to maintain that script in multiple projects.

cornedor avatar Oct 05 '23 12:10 cornedor

Adding to the list of folks that would love to see this rolled into configs instead of Rhai scripts.

Our use case is related to needing set-cookie to be returned to maintain a particular behavior. Having this set by config similar to request header propagation removes the need to maintain an additional script that could instead be defined similarly to how request headers are forwarded.

MannyPamintuanAtHeb avatar Apr 30 '24 20:04 MannyPamintuanAtHeb