How it works
What happens when you make a call
- The client looks for an access token (the short lived key SnelStart gives in exchange for your client key): first on its own instance, then in the cache. When there is none, or it expires within sixty seconds, it fetches one first.
- The token request is a form post to the token URL with
grant_type=clientkeyandclientkey=<your client key>. - The API call goes to the base URL plus the path, with
Authorization: Bearer <token>,Accept: application/jsonand, when a subscription key is set,Ocp-Apim-Subscription-Key. - A 401 on a token the client already had is answered with a new token and one repeat of the call, see below.
- A 4xx or 5xx answer throws. Anything else is decoded as JSON and returned as an array.
Where the token lives
- The token is kept until sixty seconds before its
expires_in. Withoutexpires_inin the response the client assumes 3600 seconds. - In memory, on the instance. The Laravel client is a singleton, so within one process the cache is read once, however many calls follow.
- In the cache, encrypted. The Laravel client stores the token in Laravel’s cache for the same period, so the next web request, the next job and the next process use it instead of fetching their own. The value is encrypted with the application key (
Crypt), because a cache store is often shared and this is a token for a bookkeeping. - The cache key is
snelstart.token.plus a SHA-256 hash of the token URL and the client key. Two administrations in one application never share a token, and the client key is not readable in the key. - A cached value the application cannot decrypt (a rotated
APP_KEY, something else under that key) counts as no token: it is removed and a new token is fetched. Nothing throws. php artisan cache:clearremoves the token with everything else. The next call fetches a new one; nothing else happens.- When the cache store is down, does not exist, or the application has no
APP_KEY, the client logs one warning (Snelstart token cache is not available, the token is kept in memory only: ...) and works from memory. $snelstart->forgetToken()drops the token on purpose, in memory and in the cache.- One request fetches, the others wait. When the cache has no token, the client takes a lock on the same cache store before it calls the token endpoint, and looks in the cache once more when it has the lock. Requests that arrive together on a cold cache wait at most five seconds for the first one and then use its token, so there is one token request instead of one per request. A store without locks, a lock that does not come free in those five seconds or a lock that fails: the client fetches a token without the lock. The lock never fails a call.
| Setting | Environment variable | Default |
|---|---|---|
token_cache.enabled | SNELSTART_TOKEN_CACHE | true |
token_cache.store | SNELSTART_TOKEN_CACHE_STORE | none: the default cache store |
With SNELSTART_TOKEN_CACHE=false the token only lives in memory, on the instance, as it did up to 1.1: every web request fetches its own.
The standalone client has no cache and no lock; its token lives on the instance.
A refused token: one new token, one repeat
When a call gets a 401 (the HTTP status for “not authorised”) and the token was one the client already had, from memory or from the cache, the token is no longer accepted although its lifetime has not run out. Both clients then forget the token, fetch a new one and send the same request once more.
- Only a 401 is repeated: the status with which a server refuses a request because of its credentials.
- A second 401 throws, as any other failure does.
- A 401 on a token that was fetched for this very call is not repeated: a new token would not change the answer. Check the subscription key.
- A 401 of the token endpoint is never repeated.
- Nothing else is retried: not a 429, not a 5xx.
Timeouts
Both clients wait 30 seconds for a whole request and 10 seconds for the connection, the token request included.
| Setting | Environment variable | Default |
|---|---|---|
timeout | SNELSTART_TIMEOUT | 30 |
connect_timeout | SNELSTART_CONNECT_TIMEOUT | 10 |
Both are in seconds, and 2.5 is allowed. A value that is not a positive number gives the default.
What a call returns
| Response | Result |
|---|---|
| 2xx with a JSON object or list | The decoded array |
2xx with an empty body (204, HEAD) | [] |
| 2xx with a body that is not a JSON object or list | [] |
| 4xx or 5xx | SnelstartException with that status |
What a failure looks like
Snelstart API call failed. HTTP status: 429. Response: {"message":"..."}
Failed to retrieve access_token from Snelstart. HTTP status: 401. Response: {"error":"..."}
Snelstart token response does not contain access_token.
Snelstart API config is incomplete (token_url, client_key).
- All four are a
Darvis\Snelstart\Exceptions\SnelstartException, which extendsRuntimeException. The messages are the ones above, so acatch (\RuntimeException $e)and a match on the text keep working. - Read the status, don’t parse the message.
$e->status(), and$e->getCode(), give the HTTP status of the response that caused the exception: 429 and 401 for the first two lines, the status of the 2xx token response for the third, and0for the fourth, because there was no response. - The response body is not a property of the exception. It can hold data of the administration; the message has the part that was always there, with the keys redacted.
- A 429 or a 5xx is not retried. Retry from a queued job with a backoff.
- In the Laravel client a timeout or a connection error is an
Illuminate\Http\Client\ConnectionException. That class is not aRuntimeExceptionand not aSnelstartException, so catch it separately; the package leaves it as it is, because host apps catch that class. The standalone client throws aSnelstartExceptionwithcURL error: ...and status0for the same. EchoServicedoes not throw: it catches everything, writesSnelstart Echo Resource GET failed: ...to the log withLog::error()and returns an array withsuccessset tofalse.
Where the keys go
| Secret | Travels in | Never in |
|---|---|---|
| Client key | The form body of the token request | A URL, a header |
| Subscription key | The Ocp-Apim-Subscription-Key header of API calls | A URL, the token request |
| Access token | The Authorization header of API calls; encrypted in the cache | A URL, a log line, the cache in readable form |
The response body of a failed call is part of the exception message, and a server can echo what it received. Before the message is thrown the client replaces the client key, the subscription key and the access token in it with [redacted], also in their JSON escaped and URL encoded forms. That message is what EchoService logs and what php artisan snelstart:test prints.
The rest of the response body is not filtered. It can hold data of the administration, so treat your log as you treat the bookkeeping.
Which of the two clients this is about
Darvis\Snelstart\Services\SnelstartAPI is the client for Laravel. Darvis\Snelstart\Standalone\SnelstartAPI is the same set of methods for a project without Laravel. See Standalone client for the differences.