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

Basic support for HTTP conditional requests (RFC 7232) #811

Open
hvr opened this issue Sep 21, 2017 · 4 comments
Open

Basic support for HTTP conditional requests (RFC 7232) #811

hvr opened this issue Sep 21, 2017 · 4 comments

Comments

@hvr
Copy link
Contributor

hvr commented Sep 21, 2017

An important feature of HTTP are conditional requests as specified in RFC7232.

Specifically, I'm interested in the Etag based pre-conditions, of which the most popular one IMO is the a) if-match and b) if-none-match, as these allow respectively, for a) for implementing simple transactional semantics to avoid lost-updates (i.e. update/modify/delete a resource only if its state expressed as Etag matches what the client assumes it to be in), as well as b) optimise service calls to only fetch data if there's new data to fetch, especially for services returning back 100KiBs or more worth of data this is significant, as well as for simplifying client-side logic which can choose to trust the Etag to decide equality.

Last year I mentioned this on #servant, and then I forgot about it again. So this time I'm filling an issue :-)

Btw, here's what @jkarni told me about a year ago:

[20:25:51] <hvr> does servant have any support for ETags?
[20:26:09] <hvr> I have some services where I can actually compute an etag
[22:57:57] <jkarni> hvr: servant doesn't support etags directly (aside from allowing manual request and response headers), but this seems like a fun combinator to implement - I could try sketching something out
[23:03:40] <jkarni> so maybe the API would be that the handler has type 'Handler (ETag, IO a)' (or 'Handler (ETag, Handler a)' rather than 'Handler a'?
[01:12:08] <jkarni> hvr: this is actually quite a bit more generally useful, I guess - even if you can't precompute the ETag for a If-None-Match response, you can still compute it after the fact to support If-Match concurrency patterns
@phadej
Copy link
Contributor

phadej commented Sep 21, 2017

Let's break the problem, what kind of Handler we need to wite for if-none-match

ETag -> Handler (Either NonModified a)

and we'd like API type to look like

IfNoneMatch :> Get '[JSON] a

That's a bit related to #732 where we want to be able to specify which error-codes the endpoint may result into, there we need to change a Verb too (which Get uses),


If some sees the great plan how to combines these, let's do it.

@hvr
Copy link
Contributor Author

hvr commented Sep 23, 2017

I'd like to start thinking about how we'd write the handler: on a high-level view, the handler needs to be able to receive optional conditional preconditions; for my concrete use-case I need to support the following cases:

  • neither if-none-match nor if-match in request headers
  • if-match and/or if-none-match included in request headers (RFC2716 considered it undefined behaviour if both headers were included; RFC7232 however gives clear semantics)
  • Support weak and strong etags
  • Support for if-match: */if-none-match: *
  • supporting multiple etags in if-(none-)match

As the conditional precondition logic is a bit subtle to implement, I'd claim that the logic should be provided by a combinator or servant, rather than needing each Handler to have to reinvent it.

Here's one way to model ETags:

data ETagStrength = ETagStrong | ETagWeak
data ETag = ETag !ETagStrength !Text
data ETagSet = ETagAny {- `*` -} | ETagSome (NonEmpty ETag)

data Precond = Precond
        { precondIfMatch :: Maybe ETagSet
        , precondIfNoneMatch :: Maybe ETagSet
        }

One way to model the Handler would be to shift the logic into the Handler (as @phadej seems to suggest), i.e.

:: Precond -> Handler (Either NonModified (ETag, a))

or instead move the logic outside, and instead do something like (as more or less suggested by @jkarni)

:: Handler ([ETag], ETag -> Handler a)

Where the handler returns the list of "current" ETags for a resource in order of preference (NB: these can be more than one!), and the outer logic then takes care to select one of those tags; and then either replies a 304 or instead carries on calling the nested Handler with the selected ETag.

@jkarni
Copy link
Member

jkarni commented Oct 3, 2017

Why is there a list of ETags rather than a single one?

One thought is to do something like

data HasETag = HasETag | NoETag

type Handler = Handler' 'NoETag

data Handler' (etag :: HasETag) a = ...

setETag :: ETag -> Handler 'HasETag ()
setETag = ...

The semantics of setETag would be that it short circuits (with a 304 or whatever) if the *-match* headers etc. would have it be so, and otherwise just sets the ETag header. Users would thus not be exposed to *-match headers in the request, and instead that would be supported automatically.

(Possibly setETag would take a list as argument, or the function would instead be addETag.)

@hvr
Copy link
Contributor Author

hvr commented Oct 3, 2017

Why is there a list of ETags rather than a single one?

Well, because that's what the HTTP specification supports since HTTP/1.1; each resource can have more than one "current" ETag. This is particularly important for the if-match variant which requests a specific ETag, while the server could have multiple current reversions/instances of a resource (c.f. e.g. wording in section 3.1 which implies at a server having multiple ETags available for a single resource). I've used this feature in the past to provide integrity guarantees involving multiple resources, as this is one of the few HTTP features which help with that, w/o resorting to abusing the urlpath to conflate identity and revisioning (at which point I wouldn't need Etags anymore altogether, as I'd simply use eternally cacheable urls which once existing never change their content).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants