关于使用Etags避免空中碰撞的方案可行性及Fetch存储实现疑问
1. Is this approach feasible?
Absolutely! This is a standard, industry-accepted way to implement optimistic concurrency control for RESTful APIs, and it aligns perfectly with HTTP specifications.
ETags were designed specifically for scenarios like this: to detect if a resource has changed since the client last fetched it. Returning a 412 Precondition Failed status code when the ETag mismatch is the correct HTTP response, as it clearly signals that the client's precondition (matching ETag) wasn't met. This approach avoids the overhead of pessimistic locking (like database row locks) and works seamlessly across distributed systems where multiple users/services might modify the same resource.
2. How to elegantly store the ETag from a Fetch response into an item object?
The key here is to access the response headers before parsing the JSON body (since the response body is a one-time readable stream). You can then clean up the ETag (to remove common wrapping like weak ETag prefixes W/ and surrounding quotes) and merge it with the parsed item data using object spread syntax for a clean, readable implementation.
Here's a complete example:
Fetching the item and storing the ETag
async function fetchAndStoreItem(itemId) { const response = await fetch(`/api/items/${itemId}`); if (!response.ok) { throw new Error(`Failed to fetch item: ${response.status}`); } // Extract and clean the ETag (handles weak ETags and quoted values) const rawEtag = response.headers.get('ETag'); const etag = rawEtag ? rawEtag.replace(/^W\/|"$/g, '') : null; // Parse the item data from the response body const itemData = await response.json(); // Merge the ETag into the item object return { ...itemData, etag }; }
Using the stored ETag for update requests
When sending the update, include the ETag in the If-Match header (note: most servers expect the ETag to be wrapped in quotes, so we re-add them here):
async function updateItemWithEtag(item) { try { const response = await fetch(`/api/items/${item.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', // Re-wrap the cleaned ETag in quotes for the If-Match header 'If-Match': item.etag ? `"${item.etag}"` : '' }, // Exclude the ETag from the request body unless your API expects it body: JSON.stringify({ ...item, etag: undefined }) }); if (response.status === 412) { // Handle the conflict: inform the user to refresh alert('This item has been modified by another user or process. Please refresh the page and try again.'); return null; } if (!response.ok) { throw new Error(`Failed to update item: ${response.status}`); } // If update succeeds, fetch the new ETag for future operations const newRawEtag = response.headers.get('ETag'); const newEtag = newRawEtag ? newRawEtag.replace(/^W\/|"$/g, '') : null; const updatedItem = await response.json(); return { ...updatedItem, etag: newEtag }; } catch (error) { console.error('Update error:', error); alert('An unexpected error occurred while updating the item.'); return null; } }
Key notes:
- Always access
response.headersbefore callingresponse.json()— while headers remain accessible after parsing the body, handling them first keeps the flow logical. - Cleaning the ETag removes the
W/prefix (for weak ETags) and surrounding quotes, making it easier to store and compare later. When sending it back inIf-Match, we re-add the quotes to match what most servers expect. - Using object spread (
{ ...itemData, etag }) keeps the code concise and avoids mutating the original parsed JSON object.
内容的提问来源于stack exchange,提问作者Dmitry Fadeev

