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

feat(common): http client request metadata for use in interceptors #25751

Closed
wants to merge 1 commit into from

Conversation

FDIM
Copy link
Contributor

@FDIM FDIM commented Aug 30, 2018

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

[ ] Bugfix
[x] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] angular.io application / infrastructure changes
[ ] Other... Please describe:

What is the current behavior?

Currently there is no way to configure interceptors in http client on per request basis and people (including myself) are abusing headers to disable / customize the behavior of certain interceptor (e.g. logging / authentication).

Issue Number: Fixes #18155

What is the new behavior?

This PR introduces a context option to any request that allows you to supply arbitrary data in a type safe way:

export const CacheInterceptorOptions = new HttpContextToken<{cache: boolean}>({ cache: false });
// compiles
this.http.get('/path', { context: new HttpContext().set(CacheInterceptorOptions, { cache: true }) })
this.http.get('/path', { context: new HttpContext().set(CacheInterceptorOptions, { cache: false }) })

// error
this.http.get('/path', { context: new HttpContext().set(CacheInterceptorOptions, { 
  foo: 123 // Argument of type '{ foo: number; }' is not assignable to parameter of type '{ cache: boolean; }'.
})}) 

There are also get, delete, and keys methods on HttpContext. set and remove can be chained to simplify usage.

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Other information

~~There is another PR open since last year but have seen no activity since January - probably due to other comments on the issue and lost type safety. ~~

Also, HttpInterceptorMetadata class is very generic (except for the name), maybe something like that could belong in core (or common?) package if there is a need?

EDIT: Description has been updated to match latest implementation

@FDIM FDIM force-pushed the feature/http-client-metadata branch 2 times, most recently from a439a18 to 9688390 Compare September 4, 2018 16:30
@ngbot ngbot bot added this to the needsTriage milestone Jan 25, 2019
@jasonaden jasonaden requested a review from alxhub January 25, 2019 19:29
@FDIM
Copy link
Contributor Author

FDIM commented Feb 2, 2019

I am still alive and interested in updating this PR, so let me know if I should do anything

@un33k
Copy link

un33k commented Apr 28, 2019

@FDIM You may want to resolve the conflict if you are still around & interested.

@FDIM FDIM force-pushed the feature/http-client-metadata branch from 9688390 to 59ae2ee Compare May 21, 2019 19:19
@FDIM FDIM requested a review from a team as a code owner May 21, 2019 19:19
@FDIM FDIM force-pushed the feature/http-client-metadata branch from 59ae2ee to 053e2f0 Compare May 21, 2019 19:31
@FDIM
Copy link
Contributor Author

FDIM commented May 21, 2019

PR is ready again, failure in circle ci seems to be unrelated to my change

@agroves333
Copy link

Hey guys. Where are we at with this? Any idea on why CI is failing?

@un33k
Copy link

un33k commented Jun 16, 2019

FYI - I asked the Angular team in ngConf 2019 about this feature. The answer was, that this was one of the most requested feature and they have to wait for things to settle before adding this. So, it might be very unlikely for them to accepting this PR as-is.

@BobDankert
Copy link

@un33k - what does "wait for things to settle" refer to? Just curious as I'm not quite sure what that means.

@mlc-mlapis
Copy link
Contributor

mlc-mlapis commented Jun 17, 2019

@BobDankert ... I interpret it as We are not 100% sure if this is the really right way how to it. So wait and discuss it for some time. It would be better than to introduce something and depreciate it 6 months later (with some breaking changes) because we find out something better.

@un33k
Copy link

un33k commented Jun 18, 2019

@BobDankert Check out the closing remarks at ng-conf 2019 - Timeline: 21:04.

@DutchKevv
Copy link

Any update?

Can I help somehow .. ?

@CesarD
Copy link

CesarD commented Jul 23, 2020

Why is this PR not yet approved?!
Are you owners just stalling or wth?!

@alxhub
Copy link
Member

alxhub commented Jul 23, 2020

@un33k oh that was you at ng-conf, awesome!

To expand on my answer there a little bit:

One of the big design questions here is whether metadata should be mutable or immutable. There are pros and cons to both:

  • immutable metadata avoids surprises when requests are retried via an Observable pipeline, where an interceptor will see metadata for a previously "failed" request. This can be especially confusing when a downstream interceptor added the metadata, but other aspects of the requests (e.g. the headers) do not reflect the same mutations.

  • mutable metadata would allow an interceptor to do exactly this on purpose (an obvious example here is a counter of retries on a request)

My instinct there is that immutable is the way to go. It's how the rest of the API already works, and you can always add an object to the metadata early and make use of its interior mutability if needed.

So assuming that's the direction we decide to go, the question becomes how to implement it. We have two immutable "map" types in HttpClient: HttpParams and HttpHeaders. Each has custom behavior (lazy parsing, encoding, etc) but similar functionality around append/set/delete, as well as get. Currently these are separate implementations.

If we introduce a third immutable container for metadata, it makes sense to refactor HttpParams and HttpHeaders to share a common immutable "core" that could be used to build the new container too.

@BobDankert
Copy link

Thank you @alxhub for the response. Immutable metadata would be sufficient for our projects use cases.

@mlc-mlapis
Copy link
Contributor

@alxhub

If we introduce a third immutable container for metadata, it makes sense to refactor HttpParams and HttpHeaders to share a common immutable "core" that could be used to build the new container too.

Yep, it seems like a logical and necessary step. But from the timeline point of view, do you expect some extra phases to do that? Can they significantly delay the realization?

@un33k
Copy link

un33k commented Jul 25, 2020

@alxhub Yep that was me asking the question and hopefully we can once again attend ng-config in person soon.

As far the immutability goes, I think it can be just like the HttpParams and/or HttpHeaders.
The only diff would be to have a hidden interceptor to peel the data off so it never gets on the wire.

Right now, I piggyback some metadata onto HttpHeaders and have a no-op interceptor placed at the end which is responsible to peel the data off. It works well, but it would be great to have a native solution.

@CesarD
Copy link

CesarD commented Jul 25, 2020

@alxhub

If we introduce a third immutable container for metadata, it makes sense to refactor HttpParams and HttpHeaders to share a common immutable "core" that could be used to build the new container too.

Yep, it seems like a logical and necessary step. But from the timeline point of view, do you expect some extra phases to do that? Can they significantly delay the realization?

There's been already 2 years of this PR trying to go into release... Perhaps a quick and separate implementation to make it fully available for us and leave the refactor with all the nice details for later? Just for the sake of availability and readiness.

Right now, I piggyback some metadata onto HttpHeaders

I do the exact same thing on my end. Have to add some extra header so I can use as flag for some functionality I need and then remove it so it doesn't get sent with the actual backend request.

@david0603
Copy link

bttt

@un33k
Copy link

un33k commented Nov 20, 2020

@FDIM be nice to have this ticket closed, so we can open a new one. Otherwise the new one is going to be redirected to this one and we end up in an infinite no-op loop.

@FDIM
Copy link
Contributor Author

FDIM commented Nov 20, 2020

@un33k I'm still interested to finish this, but I have no idea who can make the decision. So this is stuck until someone from core team gets back.

@CodeTroopers
Copy link

@FDIM Thanks for your cool PR. I'm also interested by this feature.

Just a details, I will rename meta and HttpInterceptorMetadata by context and HttpContext since this object could be interpreted by something else than an interceptor. I think HttpContext is a term used for this in the ASP.Net Core framework

@alxhub
Copy link
Member

alxhub commented Jan 29, 2021

@FDIM thanks for your continued work on this feature. I think there's still some work to be done on the design, but this is definitely functionality that's been missing from HttpClient for far too long. Let's iterate on this PR and fix that.

After reviewing the current version and considering the API at a high level, I think a very simple design is preferable. @CodeTroopers suggestion of the HttpContext terminology really helps frame the idea.

So here's what I'm thinking:

  1. Every HttpRequest, when created, has an HttpContext, which is effectively a metadata map. If you don't provide one, an empty default context is created internally.

  2. This context is mutable and stays with the request for as long as the request is live, even across multiple retries.

This second point is a departure from my previous intuition, which is that request metadata should use the same immutable API as the rest of HttpRequest. Thinking about the metadata as a "context" changed my mind here. The mental model would be that every request has one, and only one, context, which persists across the entire lifetime of the request, including any retry operations and any response. Through this context, the operation of interceptors can be coordinated, even across request retries.

The amount of code to implement this is surprisingly small. Here's my concept of what HttpContext might look like:

export class HttpContextKey<T> {
    constructor(readonly defaultValue: T) {}
}

export class HttpContext {
    private map = new Map<HttpContextKey<unknown>, unknown>();

    read<K>(key: HttpContextKey<K>): K {
        return this.map.has(key) ? this.map.get(key) as K : key.defaultValue;
    }

    write<K>(key: HttpContextKey<K>, value: K): void {
        this.map.set(key, value);
    }

    keys(): IterableIterator<HttpContextKey<unknown>> {
        return this.map.keys();
    }
}

That's it. Each new HttpRequest would then be either initialized with a context or have one created for it, and each HttpResponse would carry through the context of its HttpRequest. Easy, and very little new code.

Note that using a Map keyed on the instances of HttpContextKey means that there's perfect type safety here - there's no chance for two keys to collide by using the same string id. Each key also carries a default value, which allows for unambiguous reads as well as enables convenient type inference when creating a key.

Beyond the implementation above, we'll need:

  1. support for the context in the HttpClient testing framework
  2. documentation and examples in the HttpClient docs

@ngbot ngbot bot modified the milestones: needsTriage, Backlog Mar 9, 2021
@mary-poppins
Copy link

You can preview 557dae0 at https://pr25751-557dae0.ngbuilds.io/.

Copy link
Contributor

@AndrewKushnir AndrewKushnir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed-for: public-api
Reviewed-for: size-tracking

@pullapprove pullapprove bot requested a review from jelbourn March 10, 2021 01:41
Copy link
Contributor

@jessicajaniuk jessicajaniuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🍪

reviewed-for: size-tracking

@alxhub alxhub force-pushed the feature/http-client-metadata branch from 557dae0 to 4cba236 Compare March 12, 2021 19:22
@alxhub alxhub added the action: merge The PR is ready for merge by the caretaker label Mar 12, 2021
@mary-poppins
Copy link

You can preview 4cba236 at https://pr25751-4cba236.ngbuilds.io/.

A long-requested feature for HttpClient is the ability to store and retrieve
custom metadata for requests, especially in interceptors. This commit
implements this functionality via a new context object for requests.

Each outgoing HttpRequest now has an associated "context", an instance of
the HttpContext class. An HttpContext can be provided when making a request,
or if not then an empty context is created for the new request. This context
shares its lifecycle with the entire request, even across operations that
change the identity of the HttpRequest instance such as RxJS retries.

The HttpContext functions as an expando. Users can create typed tokens as instances of HttpContextToken, and
read/write a value for the key from any HttpContext object.

This commit implements the HttpContext functionality. A followup commit will
add angular.io documentation.
@mary-poppins
Copy link

You can preview 7ec7207 at https://pr25751-7ec7207.ngbuilds.io/.

@jessicajaniuk jessicajaniuk added action: merge The PR is ready for merge by the caretaker and removed action: merge The PR is ready for merge by the caretaker labels Mar 13, 2021
@jessicajaniuk
Copy link
Contributor

Caretaker Note: This has been merged.

@un33k
Copy link

un33k commented Mar 15, 2021

@FDIM You finally did it! Thank you very much for your perseverance, and ultimately this contribution.

@FDIM
Copy link
Contributor Author

FDIM commented Mar 15, 2021

I'm so happy that this finally made it into the framework and looking forward to cleaning up misused headers!

@jcimoch
Copy link

jcimoch commented Mar 19, 2021

so many years but it's finally there!!!!
thanks @FDIM

@nasreddineskandrani
Copy link

great work! this is big :)

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Apr 20, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker aio: preview area: common/http cla: yes target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow passing misc data to http client options