Hi,
I would like to report an in consistency regarding If-Modified-Since / Last-Modified between the API documentation and the actual behaviour.
The API documentation says:
"Each response has "ETag" and "Last-Modified" headers. If your client supplies the proper header in the request to prove that the data have not changed since your previous request ("If-None-Match" and "If-Modified-Since", respectively), then you will receive a 304 Not Modified response code, telling you that it is safe and correct to use your cached version."
ETag / If-None-Match works exactly as documented. If-Modified-Since / Last-Modified does not. Three distinct observations:
1. The origin ignores If-Modified-Since regardless of value
Probes below omit If-None-Match so only the date header is evaluated (RFC 7232 §6 gives ETag precedence when both are present):
# 1. Verbatim value from a prior Last-Modified response.
$ curl -s -o /dev/null -w '%{http_code}\n' \
-H 'If-Modified-Since: Friday, 17-Apr-2026 11:05:26 GMT+0000' \
https://api.chess.com/pub/player/snowycat
200
# 2. Same instant re-emitted in RFC 7231 IMF-fixdate form.
$ curl -s -o /dev/null -w '%{http_code}\n' \
-H 'If-Modified-Since: Fri, 17 Apr 2026 11:05:26 GMT' \
https://api.chess.com/pub/player/snowycat
200
# 3. A date 30 days in the FUTURE. Per RFC 7232 §3.3 the server MUST
# return 304 here (the resource cannot have been modified since a
# future instant).
$ curl -s -o /dev/null -w '%{http_code}\n' \
-H 'If-Modified-Since: Sun, 17 May 2026 11:05:26 GMT' \
https://api.chess.com/pub/player/snowycat
200
Same shape on /pub/player/{u}, /pub/player/{u}/stats, /pub/player/{u}/games/archives, and /pub/match/{id}. For comparison, If-None-Match with the same resource returns 304 with a matching ETag and 200 with a bogus one — so the conditional-GET path is alive, it just doesn't consult If-Modified-Since.
2. The Last-Modified value doesn't track content modification
snowycat is a closed account so the resource's content is static. The ETag W/"09ef210fc806b620027024c8ed241933" is byte-identical across multiple fetches separated by days. Yet the Last-Modified value reports a time from earlier this morning.
Same shape across a range of historic matches. Fetched a day apart, these ETags are all byte-identical:
/pub/match/85 W/"f2392d655d3950ca92ce842dd730a453" last-modified: Thursday, 16-Apr-2026 17:07:4x GMT+0000
/pub/match/242 W/"8cf334e08ce33dd8f6c7b55d289561ad" last-modified: Thursday, 16-Apr-2026 17:07:4x GMT+0000
/pub/match/1346 W/"8bd97ff4ab6a03f7da2ee0b8683229bd" last-modified: Thursday, 16-Apr-2026 17:08:1x GMT+0000
/pub/match/3055 W/"e46352cbc0007bc81911e6d1b213b287" last-modified: Thursday, 16-Apr-2026 17:16:3x GMT+0000
/pub/match/11495 W/"d88fe2ad3ef6d31644d16b712abeb551" last-modified: Friday, 10-Apr-2026 13:18:19 GMT+0000
/pub/match/12 W/"4e2c65fc5eee4b2ecfbd66c2bc85cd73" last-modified: Thursday, 16-Apr-2026 23:45:4x GMT+0000
These matches are all long-finished (match 12 is from Chess.com's earliest days). The ETag confirms the bodies haven't changed — yet Last-Modified values are scattered across the past week, not years ago. Whatever this value is tracking (edge-cache refresh time, perhaps), it isn't origin-resource modification.
The upshot: a client that honestly echoed the received timestamp back in If-Modified-Since would get 200s forever, because the server's idea of the resource's "modification time" on the next request is going to be later than any value it previously emitted.
3. The Last-Modified value isn't a valid HTTP-date
A typical response:
GET https://api.chess.com/pub/player/snowycat
etag: W/"09ef210fc806b620027024c8ed241933"
last-modified: Friday, 17-Apr-2026 11:05:26 GMT+0000
cache-control: public, max-age=5
RFC 7231 §7.1.1.1 defines exactly three legal HTTP-date forms:
Form
Example
IMF-fixdate (preferred)
Fri, 17 Apr 2026 11:05:26 GMT
RFC 850 (obsolete)
Friday, 17-Apr-26 11:05:26 GMT
asctime
Fri Apr 17 11:05:26 2026
The value Friday, 17-Apr-2026 11:05:26 GMT+0000 matches none:
Full weekday name rules out IMF-fixdate and asctime.
4-digit year in hyphen-separated form rules out RFC 850, which mandates a 2-digit year.
GMT+0000 isn't a legal HTTP-date timezone — RFC 7231 requires the literal string GMT.
Strict RFC 7231 parsers reject it outright (for example Java's DateTimeFormatter.RFC_1123_DATE_TIME throws DateTimeParseException, and typed-header wrappers built on it silently return None). More lenient parsers often parse successfully but drop the timezone — Python's email.utils.parsedate_to_datetime returns a naive datetime for this input versus a UTC-aware datetime for correctly-formed IMF-fixdate, a subtle footgun for code that does datetime arithmetic on the result.