Add check to omit `DateTime` from etag calculation
Ignores any json property named `DateTime` when calculating
the etag value of an HTTP response as per the updated
Redfish Spec (section 6.5: ETags)
Tested:
- Redfish Service Validator passes
- Tested on romulus:
1. GET resource with a "DateTime" field
```
curl -k -H "X-Auth-Token: $XAUTH_TOKEN" "https://$BMC/redfish/v1/TaskService" \
--etag-save ./etag.txt -v
...
< etag: "6A4CE897"
...
{
"@odata.id": "/redfish/v1/TaskService",
"@odata.type": "#TaskService.v1_1_4.TaskService",
"CompletedTaskOverWritePolicy": "Oldest",
"DateTime": "2025-07-23T17:08:20+00:00",
"Id": "TaskService",
"LifeCycleEventOnTaskStateChange": true,
"Name": "Task Service",
"ServiceEnabled": true,
"Status": {
"State": "Enabled"
},
"Tasks": {
"@odata.id": "/redfish/v1/TaskService/Tasks"
}
```
2. GET same resource again later, etag is same as before
```
curl -k -H "X-Auth-Token: $XAUTH_TOKEN" "https://$BMC/redfish/v1/TaskService" \
--etag-save ./etag.txt -v
...
< etag: "6A4CE897"
...
{
"@odata.id": "/redfish/v1/TaskService",
"@odata.type": "#TaskService.v1_1_4.TaskService",
"CompletedTaskOverWritePolicy": "Oldest",
"DateTime": "2025-07-23T17:10:48+00:00",
"Id": "TaskService",
"LifeCycleEventOnTaskStateChange": true,
"Name": "Task Service",
"ServiceEnabled": true,
"Status": {
"State": "Enabled"
},
"Tasks": {
"@odata.id": "/redfish/v1/TaskService/Tasks"
}
```
"DateTime" is the only value to have changed, but since
it is ignored the etag did not change
3. GET with if-none-match returns 304
```
curl -k -H "X-Auth-Token: $XAUTH_TOKEN" "https://$BMC/redfish/v1/TaskService" \
--etag-save ./etag.txt --etag-compare ./etag.txt -v
...
> if-none-match: "6A4CE897"
...
< HTTP/2 304
< allow: GET
< odata-version: 4.0
< strict-transport-security: max-age=31536000; includeSubdomains
< pragma: no-cache
< cache-control: no-store, max-age=0
< x-content-type-options: nosniff
< etag: "6A4CE897"
< date: Wed, 23 Jul 2025 17:14:39 GMT
< content-length: 0
<
...
```
Change-Id: I51f7668e75719c69c55535e4a1e48c8bae7c9488
Signed-off-by: Corey Ethington <cethington@coreweave.com>
diff --git a/http/complete_response_fields.hpp b/http/complete_response_fields.hpp
index 41314b8..7b6a509 100644
--- a/http/complete_response_fields.hpp
+++ b/http/complete_response_fields.hpp
@@ -67,7 +67,7 @@
BMCWEB_LOG_INFO("Response: {}", res.resultInt());
addSecurityHeaders(res);
- res.setHashAndHandleNotModified();
+ res.setResponseEtagAndHandleNotModified();
if (res.jsonValue.is_structured())
{
using http_helpers::ContentType;
diff --git a/http/http2_connection.hpp b/http/http2_connection.hpp
index efd9225..d394889 100644
--- a/http/http2_connection.hpp
+++ b/http/http2_connection.hpp
@@ -321,12 +321,12 @@
return 0;
}
}
- std::string_view expected =
+ std::string_view expectedEtag =
thisReq.getHeaderValue(boost::beast::http::field::if_none_match);
- BMCWEB_LOG_DEBUG("Setting expected hash {}", expected);
- if (!expected.empty())
+ BMCWEB_LOG_DEBUG("Setting expected etag {}", expectedEtag);
+ if (!expectedEtag.empty())
{
- asyncResp->res.setExpectedHash(expected);
+ asyncResp->res.setExpectedEtag(expectedEtag);
}
handler->handle(it->second.req, asyncResp);
return 0;
diff --git a/http/http_connection.hpp b/http/http_connection.hpp
index 64aac00..8e68bf5 100644
--- a/http/http_connection.hpp
+++ b/http/http_connection.hpp
@@ -440,11 +440,11 @@
{
return;
}
- std::string_view expected =
+ std::string_view expectedEtag =
req->getHeaderValue(boost::beast::http::field::if_none_match);
- if (!expected.empty())
+ if (!expectedEtag.empty())
{
- asyncResp->res.setExpectedHash(expected);
+ asyncResp->res.setExpectedEtag(expectedEtag);
}
handler->handle(req, asyncResp);
}
diff --git a/http/http_response.hpp b/http/http_response.hpp
index 0d32982..81c664a 100644
--- a/http/http_response.hpp
+++ b/http/http_response.hpp
@@ -77,7 +77,9 @@
Response() = default;
Response(Response&& res) noexcept :
response(std::move(res.response)), jsonValue(std::move(res.jsonValue)),
- expectedHash(std::move(res.expectedHash)), completed(res.completed)
+ requestExpectedEtag(std::move(res.requestExpectedEtag)),
+ currentOverrideEtag(std::move(res.currentOverrideEtag)),
+ completed(res.completed)
{
// See note in operator= move handler for why this is needed.
if (!res.completed)
@@ -102,7 +104,8 @@
}
response = std::move(r.response);
jsonValue = std::move(r.jsonValue);
- expectedHash = std::move(r.expectedHash);
+ requestExpectedEtag = std::move(r.requestExpectedEtag);
+ currentOverrideEtag = std::move(r.currentOverrideEtag);
// Only need to move completion handler if not already completed
// Note, there are cases where we might move out of a Response object
@@ -223,10 +226,21 @@
jsonValue = nullptr;
completed = false;
- expectedHash = std::nullopt;
+ requestExpectedEtag = std::nullopt;
+ currentOverrideEtag = std::nullopt;
}
- std::string computeEtag() const
+ void setCurrentOverrideEtag(std::string_view newEtag)
+ {
+ if (currentOverrideEtag)
+ {
+ BMCWEB_LOG_WARNING(
+ "Response override etag was incorrectly set twice");
+ }
+ currentOverrideEtag = newEtag;
+ }
+
+ std::string getCurrentEtag() const
{
// Only set etag if this request succeeded
if (result() != http::status::ok)
@@ -238,6 +252,12 @@
{
return "";
}
+
+ if (currentOverrideEtag)
+ {
+ return currentOverrideEtag.value();
+ }
+
size_t hashval = std::hash<nlohmann::json>{}(jsonValue);
return "\"" + intToHexString(hashval, 8) + "\"";
}
@@ -283,26 +303,35 @@
return ret;
}
- void setHashAndHandleNotModified()
+ void setResponseEtagAndHandleNotModified()
{
// Can only hash if we have content that's valid
if (jsonValue.empty() || result() != http::status::ok)
{
return;
}
- size_t hashval = std::hash<nlohmann::json>{}(jsonValue);
- std::string hexVal = "\"" + intToHexString(hashval, 8) + "\"";
+ std::string hexVal = getCurrentEtag();
addHeader(http::field::etag, hexVal);
- if (expectedHash && hexVal == *expectedHash)
+ if (requestExpectedEtag && hexVal == *requestExpectedEtag)
{
jsonValue = nullptr;
result(http::status::not_modified);
}
}
- void setExpectedHash(std::string_view hash)
+ std::optional<std::string_view> getExpectedEtag() const
{
- expectedHash = hash;
+ return requestExpectedEtag;
+ }
+
+ void setExpectedEtag(std::string_view etag)
+ {
+ if (requestExpectedEtag)
+ {
+ BMCWEB_LOG_WARNING(
+ "Request expected etag was incorrectly set twice");
+ }
+ requestExpectedEtag = etag;
}
OpenCode openFile(
@@ -347,7 +376,8 @@
}
private:
- std::optional<std::string> expectedHash;
+ std::optional<std::string> requestExpectedEtag;
+ std::optional<std::string> currentOverrideEtag;
bool completed = false;
std::function<void(Response&)> completeRequestHandler;
};