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

[BUG]: Request caching returns invalid responses for GET endpoints #337

Open
1 task done
stestagg opened this issue Aug 2, 2023 · 7 comments
Open
1 task done
Labels
hacktoberfest Issues for participation in Hacktoberfest Status: Triage This is being looked at and prioritized Status: Up for grabs Issues that are ready to be worked on by anyone Type: Bug Something isn't working as documented Type: Documentation Improvements or additions to documentation

Comments

@stestagg
Copy link
Contributor

stestagg commented Aug 2, 2023

What happened?

The standard GET request caching (for example when calling repos.getContent), doesn't handle the case where the same resource is requested with different media types.

If you request a file with format 'raw', then a file with format 'json' (for example), then the content of the response will depend on whatever was most recently cached.

It could be argued that this is a bug/issue with the github rest api implementation (issuing the same etag for both formats), or some issue with browser header handling, but it's not clear from the documentation, or a quick look at the code, how to disable the caching behaviour for octokit rest methods.

I've put together a jsfiddle showing the behaviour:
https://jsfiddle.net/qvtkmnwp/44/ (click go, with browser request caching disabled/enabled to see difference).

Versions

https://esm.sh/@octokit/[email protected]

Relevant log output

▶️▶️▶️ WITH CACHE ENABLED 
Request 1 (raw): etag: W/"495a13f66a7b267e5a3d9b7ac795289c60d79003"
Request 2 (json): etag: W/"495a13f66a7b267e5a3d9b7ac795289c60d79003"
Request 1 data type: object (expecting string)
Request 2 data type: object (expecting object)

----- Request 1 data (raw) (expect text of file) ----
[object Object]

----- Request 2 data (json) (expect [object Object])----
[object Object]


▶️▶️▶️ WITH BROWSER CACHE DISABLED:
Request 1 (raw): etag: "495a13f66a7b267e5a3d9b7ac795289c60d79003"
Request 2 (json): etag: W/"495a13f66a7b267e5a3d9b7ac795289c60d79003"
Request 1 data type: string (expecting string)
Request 2 data type: object (expecting object)

----- Request 1 data (raw) (expect text of file) ----
# rest.js

> GitHub REST API client for JavaScript

[![@latest](https://img.shields.io/npm/v/@octoki

----- Request 2 data (json) (expect [object Object])----
[object Object]

Code of Conduct

  • I agree to follow this project's Code of Conduct
@stestagg stestagg added Status: Triage This is being looked at and prioritized Type: Bug Something isn't working as documented labels Aug 2, 2023
@github-actions
Copy link
Contributor

github-actions bot commented Aug 2, 2023

👋 Hi! Thank you for this contribution! Just to let you know, our GitHub SDK team does a round of issue and PR reviews twice a week, every Monday and Friday! We have a process in place for prioritizing and responding to your input. Because you are a part of this community please feel free to comment, add to, or pick up any issues/PRs that are labled with Status: Up for grabs. You & others like you are the reason all of this works! So thank you & happy coding! 🚀

@wolfy1339
Copy link
Member

I can't find any code doing any cache handling on the Octokit side.
This is all handled on GitHub's server side.

@stestagg
Copy link
Contributor Author

stestagg commented Aug 3, 2023

I can't find any code doing any cache handling on the Octokit side.

I think this is ultimately the problem. The issue here falls into a bit of a gap in how caching is specified/implemented. It seems that If-None-Match etag comparisons aren't required to honour the Vary headers, and browsers don't do that for other reasons. So:

  • GH correctly populates the Vary header, and doesn't change the Etag value based on the Accept field
  • And browsers don't key their caches based on the values of the Vary header (for performance reasons it seems), which doesn't seem technically incorrect(https://stackoverflow.com/questions/60799116/if-none-match-headers-ignore-content-type-and-vary) when using etags, but isn't ideal.
  • GH then returns a 304 because the etag matches in both requests, and the browser serves the cached content (wrong content type)

So (as far as I can tell) there's no obvious finger of blame to point to any particular system, it's just a quirk of how REST APIs implemented like this expose this issue.

The result is that this client library appears to be wrong, as evidenced by the fiddle. Allowing the developer to control caching is fairly standard for a REST clients, as caching is always complex, and escape-hatches are needed way more often than feels ideal.

The underlying fetch/request APIs allow for cache behaviour to be controlled, but it's not clear through the rest.js code or the docs how to achieve this.

In the end, I was able to hack the issue by adding:
headers: {'If-None-Match': ''},
to the method call, but this was only possible by stepping through the code to understand the injection points.

So maybe this is just a gap in the docs? or it might be nice to have a tiny addition to tune cache behaviour on a per-call basis?

@nickfloyd
Copy link
Contributor

Hey @stestagg thanks for the legwork here! ❤️ Would you happen to have a code sandbox or a repo with source where you proved this out? I'd love to get, at least, some docs on this behavior.

As always, you're more than welcome to add the docs and submit a PR. I feel like your findings could potentially be beneficial to lots of folks!

@nickfloyd nickfloyd added Status: Up for grabs Issues that are ready to be worked on by anyone Type: Documentation Improvements or additions to documentation labels Aug 4, 2023
@wolfy1339
Copy link
Member

There are docs that mention request options, right in the Node usage.
https://octokit.github.io/rest.js/v19#custom-requests

@stestagg
Copy link
Contributor Author

stestagg commented Aug 7, 2023

Hi @nickfloyd, thanks for the message!

There's a jsfiddle in the report: https://jsfiddle.net/qvtkmnwp/44/

The fiddle works for me in firefox and safari on osx (I can't see why it wouldn't reproduce in other envs too).

In essence, when calling (note the different format):

let res1 = await ok.repos.getContent({
    owner: 'octokit',
    repo: 'rest.js',
    ref: 'main',
    path: 'README.md',
    mediaType: {format: 'json'}
  });
let res2 = await ok.repos.getContent({
    owner: 'octokit',
    repo: 'rest.js',
    ref: 'main',
    path: 'README.md',
    mediaType: {format: 'raw'}
  });

One of res1 or res2 will hold the wrong value. (depending on what's cached :), also, often browsers will disable request caching when dev tools are open!!).

I did think of trying to contribute back with a fix, but honestly I'm not up to speed with the best knowledge here wrt browser caching.
I found that adding:
headers: {'If-None-Match': ''},
worked for me, but there is almost certainly a better way if this were to be done as a formal fix. I know there are cache control headers, and that there are rules on when/how they are allowed/enforced, but it's all a bit foreign to me.

Thanks

Steve

@gr2m
Copy link
Contributor

gr2m commented Aug 7, 2023

I can't find any code doing any cache handling on the Octokit side

The only cache behavior we do is to throw an error in order to implement caching: https://github.com/octokit/request.js/blob/9e00cf5695f9e3bf100e2b9123c549b7fae9985e/src/fetch-wrapper.ts#L90-L100. But there is no official plugin for caching yet, there really should be though, it's probably one of the last recommended best practices that is not yet codified in the JavaScript Octokit SDK

@nickfloyd nickfloyd added the hacktoberfest Issues for participation in Hacktoberfest label Sep 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
hacktoberfest Issues for participation in Hacktoberfest Status: Triage This is being looked at and prioritized Status: Up for grabs Issues that are ready to be worked on by anyone Type: Bug Something isn't working as documented Type: Documentation Improvements or additions to documentation
Projects
Status: 🔥 Backlog
Development

No branches or pull requests

4 participants