Skip to content

Add persistent session support with automatic token renewal and retry#156

Merged
msyk merged 1 commit into
msyk:masterfrom
MjukBiltvatt:master
May 25, 2026
Merged

Add persistent session support with automatic token renewal and retry#156
msyk merged 1 commit into
msyk:masterfrom
MjukBiltvatt:master

Conversation

@filiptorphage-mjuk
Copy link
Copy Markdown
Contributor

Overview

By default the library logs in and out on every single database operation, which means a full authentication round-trip each time. The library already supports performing several Data API calls under one PHP request, but this still means a full authentication round-trip each PHP request. This PR adds opt-in support for sharing a session token across PHP requests, so only the first request pays the cost of logging in — every subsequent request on the same server reuses the cached token instead.

This PR adds two features:

  1. Persistent sessions — This is what allows several PHP requests to share a single Data API session token. This is entirely opt-in, and is done by specifying the new sessionCache constructor parameter on FMDataAPI.
  2. Retry mechanism — Another opt-in feature, which allow a request to retry once more with a new Data API session, if the current session were invalidated in the middle of a PHP request. This is useful when using startCommunication(). Without a session cache specified in the FMDataAPI contructor, it requres explicitely calling setRetryOnAccessTokenInvalidation(true).

The persistent sessions have the retry mechanism enabled by design, as it would not make sense to use persistent sessions without any way of renewing an expired session.

Omitting both the sessionCache constructor parameter and setRetryOnAccessTokenInvalidation(true) method keeps all existing behavior intact.

This PR provides a default cache backend, APCu, for those that have it enabled. It is deliberately not a requirement of the package, and will only work for those that have APCu enabled.


How to use it

Pass a cache backend to the constructor and the library handles the rest — storing the token after login, reusing it on subsequent requests, and automatically recovering if the token goes stale.

$fm = new FMDataAPI(
    solution: 'MyDatabase',
    user: 'admin',
    password: 'secret',
    host: 'filemaker.example.com',
    sessionCache: new ApcuSessionCache(),
);

// The first request logs in and caches the token.
// Subsequent requests from any PHP worker on this server reuse it.
$fm->startCommunication();
$records = $fm->layout('Contacts')->query();
$fm->endCommunication();
// endCommunication() renews the cache TTL instead of logging out.

New behavior of startCommunication() and endCommunication() whenever a session cache is specified:

  • startCommunication() — This stored the access token locally as normal. Without this, the request token would be read from the cache on each request, which is still fast.
  • endCommunication() — Same as before, with the addition of being the only location where the cached item's TTL being updated. The details of this choice will be elaborated on further down.

The default cache TTL is set to 840 seconds, which leaves a 60 seconds buffer before Data API access token would expire on the FileMaker Server-side.

This PR supports custom cache backends through two different approaches:

  1. Extend AbstractSessionCache (this already implements SessionCacheInterface, used to reduce boilerplate)
  2. Implement SessionCacheInterface.

See ApcuSessionCache on how to extend AbstractSessionCache. If you want to implement SessionCacheInterface directly, see both ApcuSessionCache and AbstractSessionCache.


New files

  • AbstractSessionCache.php — public API. Includes properties and setter functions, which reduces boilerplate for custom cache implemetations.
  • ApcuSessionCache.php — public API. Ready-made APCu implementation. Throws if APCu isn't available.
  • SessionCacheInterface.php — public API. Five methods: get, set (with TTL), delete, setTtl, setKey. These will need to be implemented if you want to extend AbstractSessionCache.

Changes to existing files

Review carefully

src/Supporting/CommunicationProvider.php — the core of this PR. Most of the new logic lives here.

  • New $resumeScopeAfterReauth property. This only serves as a way to remember if keepAuth were set before renewing the session through the new retry-mechanism.
  • New $retryOnAccessTokenInvalidation property. This tells the application if it should retry a failed call if an Data API session token invalidation error occured.
  • New $sessionStore property. The instance of the session cache that is used for persistent sessions.
  • startCommunication() (moved) — sets keepAuth to the return value of login(), so it's only ever true when login actually succeeded.
  • endCommunication() (moved) — same as the old endCommunication(), but if persistent session are used: If the cached token matches ours, renews the TTL and skips the server logout. If tokens differ, another worker has replaced the cached token; ours is an orphan and gets DELETEd. Falls through to logout() when no cache is configured. Reasoning further down.
  • login() — checks the cache before attempting an HTTP login. After a fresh login, stores the token in the cache. Changed to call callRestAPIWithoutRetry internally.
  • logout() — three new guards: returns early if accessToken is null; returns early if the token matches the cache; wraps the DELETE in try/finally.
  • callRestAPI() / callRestAPIWithoutRetry() — the original callRestAPI is renamed to callRestAPIWithoutRetry and made protected. The new callRestAPI wraps it with retry logic on 952/112, clearing the cache, re-logging in, and retrying once. The original exception is available via getPrevious().

Mechanical changes — safe to skim

  • FMDataAPI.php — new $sessionCache constructor parameter; startCommunication() and endCommunication() delegate to the provider; new setRetryOnAccessTokenInvalidation() method.
  • FileMakerLayout.phpstartCommunication() and endCommunication() delegate to the provider.
  • TestProvider.php — constructor updated to accept and forward SessionCacheInterface|null to the parent.

Behavioural changes for existing users

These apply regardless of whether a session cache is configured.

Please verify all of these.

  • logout() null guard — previously, calling logout() with no active session would fire a DELETE /sessions/ request with a null token in the URL. Before in the case when throwExceptionInError was set, this would always throw an exception since this is an invalid operation (it creates a malformed header). Now it immediately returns. This should never occur in the first place, and if accessToken is already null, then we should already be logged out.
  • logout() try/finallyaccessToken is now always nulled after a DELETE attempt, even if the request throws. Previously a thrown exception left it set.
  • startCommunication() login-failure fix — This one were divided into two files. In FMDataAPI.php, then keepAuth were never set to false in non-throwing mode. In FileMakerLayout.php then keepAuth were never set to false in case of a failed login at all. Now, in case of any error, then keepAuth is set to false and keepAuth is only set to true on a successful login.
  • endCommunication() — When a session cache is enabled, this method now only logs out sessions where the local accessToken does not match the cached session token. This is to ensure that one, and only one, session token is stored in the cache. This is also the only place where the TTL of the cached token is renewed.
  • Retry on 952/112 — opt-in. Without a session cache, call setRetryOnAccessTokenInvalidation(true) to enable it. With a session cache it activates automatically, since a persistent session that can't recover from an expired token is effectively broken. The retry runs in a fresh session — session-scoped state like global fields set before the failure won't carry over.

Known limitations

Currently, when a session cache is used, the value stored in the key-value pair only has its TTL renewed upon calling endCommunication(). This is a deliberate choice as APCu is not atomic and this minimizes the risk of race conditions (rather than setting it after each callRestAPI (which increases the surface for race conditions). This means that if an application does not call endCommunication() within 14 minutes from opening the session, then the session will be invalidated in the cache and a new session will be started on the next FileMaker Data API request. This is not an issue, since after 14 minutes, the retry-mechanism catches the invalidated token and retries with a new session.


Race conditions and stale tokens

If multiple workers miss the cache simultaneously (on cold start or after a 952 clears it), each logs in independently and gets its own token. Last writer wins the cache; the rest become orphans and idle out within FM Server's 15-minute timeout. endCommunication() mitigates this by explicitly DELETEing a token when it finds the cache has already moved on, but workers that crash mid-scope won't get that cleanup. If your FM Server has a low per-user session limit and you anticipate frequent simultaneous cold starts, consider warming the cache from a single entry point before traffic arrives.

Under sustained cache failure in endCommunication() with high concurrency, orphaned tokens could approach FileMaker's session cap (error 953).

Regarding tests

I have modified TestProvider to take in $sessionCache and replaced the callRestAPI() override to now override callRestAPIWithoutRetry() (as I renamed that function). However, I'm not sure how the tests in this repositories are used, and I'm not sure how to even run them, so I'll leave the testing to you (msyk). If you feel the need to add any tests to this, feel free to do so.

Other

As we have already discussed, this PR treats both FileMaker error codes 952 and 112 as a Data API session token invalidation. This was because I could consistently replicate the FileMaker Server returning error code 112 when a Data API session token were invalidated during FileMaker Server Data API request.

@msyk
Copy link
Copy Markdown
Owner

msyk commented May 25, 2026

Good work! I'll consider about the testing anyway.

@msyk msyk merged commit b8bfa06 into msyk:master May 25, 2026
62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants