Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add flexibility to ETag generation with sources
- Refactored `EtagManager` to support dynamic ETag generation based on configurable sources (`updated_at`).
- Introduced `generateEtagFromTables` with a `source` parameter for flexibility in determining the source of truth.

This update prepares the app for future scalability and allows switching between different ETag generation strategies.
  • Loading branch information
eiresendez committed Dec 5, 2024
commit d748403ba9da105358ab0b575e1abaf89ad1e324
92 changes: 69 additions & 23 deletions ProcessMaker/Http/Middleware/Etag/HandleEtag.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,36 @@ public function handle(Request $request, Closure $next): Response
return $next($request);
}

// Handle response.
$response = $next($request);
// Check if specific tables are defined for the route and calculate ETag.
$etag = $this->generateEtagFromTablesIfNeeded($request);

// Check if the response is cacheable.
if (!$this->isCacheableResponse($response)) {
return $response; // Skip ETag for non-cacheable responses.
}
// If the client has a matching ETag, return a 304 response.
// Otherwise, continue with the controller execution.
$response = $etag && $this->etagMatchesRequest($etag, $request)
? $this->buildNotModifiedResponse($etag)
: $next($request);

// Generate ETag for the response.
$etag = EtagManager::getEtag($request, $response);
// Add the pre-calculated ETag to the response if available.
if ($etag) {
// Set the ETag header.
$response->setEtag($etag);
$response = $this->setEtagOnResponse($response, $etag);
}

// Get and strip weak ETags from request headers.
$noneMatch = array_map([$this, 'stripWeakTags'], $request->getETags());
// If no ETag was calculated from tables, generate it based on the response.
if (!$etag && $this->isCacheableResponse($response)) {
$etag = EtagManager::getEtag($request, $response);
if ($etag) {
$response = $this->setEtagOnResponse($response, $etag);

// Compare ETags and set response as not modified if applicable.
if (in_array($etag, $noneMatch)) {
$response->setNotModified();
// If the client has a matching ETag, set the response to 304.
if ($this->etagMatchesRequest($etag, $request)) {
$response = $this->buildNotModifiedResponse($etag);
}
}
}

return $response;
}

/**
* Remove the weak indicator (W/) from an ETag.
*/
private function stripWeakTags(string $etag): string
{
return str_replace('W/', '', $etag);
}

/**
* Determine if a response is cacheable.
*/
Expand All @@ -67,4 +63,54 @@ private function isCacheableResponse(Response $response): bool
return in_array($response->getStatusCode(), $cacheableStatusCodes)
&& !str_contains($cacheControl, 'no-store');
}

/**
* Generate an ETag based on the tables defined in the route, if applicable.
*/
private function generateEtagFromTablesIfNeeded(Request $request): ?string
{
$tables = $request->route()->defaults['etag_tables'] ?? null;

return $tables ? EtagManager::generateEtagFromTables(explode(',', $tables)) : null;
}

/**
* Check if the ETag matches the request.
*/
private function etagMatchesRequest(string $etag, Request $request): bool
{
$noneMatch = array_map([$this, 'stripWeakTags'], $request->getETags());

return in_array($etag, $noneMatch);
}

/**
* Build a 304 Not Modified response with the given ETag.
*/
private function buildNotModifiedResponse(string $etag): Response
{
$response = new Response();
$response->setNotModified();
$response->setEtag($etag);

return $response;
}

/**
* Set the ETag on a given response.
*/
private function setEtagOnResponse(Response $response, string $etag): Response
{
$response->setEtag($etag);

return $response;
}

/**
* Remove the weak indicator (W/) from an ETag.
*/
private function stripWeakTags(string $etag): string
{
return str_replace('W/', '', $etag);
}
}
27 changes: 27 additions & 0 deletions ProcessMaker/Http/Resources/Caching/EtagManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

Expand Down Expand Up @@ -41,4 +43,29 @@ private static function defaultGetEtag(Response $response): string
{
return md5(auth()->id() . $response->getContent());
}

/**
* Generate an ETag based on the latest update timestamps from multiple tables.
*/
public static function generateEtagFromTables(array $tables, string $source = 'updated_at'): string
{
// Fetch the latest update timestamp from each table.
// If the source is 'etag_version', use a cached version key as the source of truth.
$lastUpdated = collect($tables)->map(function ($table) use ($source) {
if ($source === 'etag_version') {
// This is not currently implemented but serves as a placeholder for future flexibility.
// The idea is to use a cached version key (e.g., "etag_version_table_name") as the source of truth.
// This would allow us to version the ETag dynamically and invalidate it using model observers or other mechanisms.
// If implemented, observers can increment this version key whenever the corresponding table is updated.
return Cache::get("etag_version_{$table}", 0);
}

// Default to the updated_at column in the database.
return DB::table($table)->max('updated_at');
})->max();

$etag = md5(auth()->id() . $lastUpdated);

return (string) Str::of($etag)->start('"')->finish('"');
}
}
1 change: 1 addition & 0 deletions routes/v1_1/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

// Route to show the screen of a task
Route::get('/{taskId}/screen', [TaskController::class, 'showScreen'])
->defaults('etag_tables', 'screens,screen_versions')
->name('show.screen');

// Route to show the interstitial screen of a task
Expand Down