Skip to content
Closed
Changes from all commits
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
72 changes: 60 additions & 12 deletions src/BaseApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ protected function authenticateRequest(): self

protected function applyRequestEncoding(PendingRequest $request): PendingRequest
{
switch (strtolower($this->requestEncoding)) {
switch (strtolower((string) $this->requestEncoding)) {
case 'asform':
case 'form':
case 'x-www-form-urlencoded':
Expand Down Expand Up @@ -128,12 +128,20 @@ protected function executeCall(): self
return $this;
}

return $this
->makeRequest()
->authenticateRequest()
->setResponse(
$this->request->{$this->requestMethod}($this->getEndpoint(), $this->getRequestData())
);
try {
return $this
->makeRequest()
->authenticateRequest()
->setResponse(
$this->request->{$this->requestMethod}($this->getEndpoint(), $this->getRequestData())
);
} catch (\Throwable $e) {
// a temporary request encoding must not leak into the next call
// when the request itself fails (e.g. a connection timeout)
$this->restoreRequestEncoding();

throw $e;
}
}

protected function fetchBearerToken(array $requestData = [], string $requestMethod = 'post'): self
Expand Down Expand Up @@ -175,7 +183,11 @@ protected function getCacheKey(): string
throw new Exception('no endpoint was specified');
}

return static::class.':'.$this->baseUrl.'/'.$this->endpoint.':'.md5(serialize($this->requestData));
// use the resolved endpoint: getEndpoint() consumes ":param" entries from
// the request data, so building the key from the raw endpoint before and
// after resolution would produce two different keys for the same call
// (cache lookups for keyed endpoints could then never hit)
return static::class.':'.$this->baseUrl.'/'.$this->getEndpoint().':'.md5(serialize($this->requestData));
}

protected function getHeaders(): array
Expand Down Expand Up @@ -223,11 +235,22 @@ protected function getTokenCacheKey(): string

protected function loadResponseFromCache(): bool
{
if ($existsInCache = $this->useCache && Cache::has($this->getCacheKey())) {
$this->response = Cache::get($this->getCacheKey());
if (!$this->useCache) {
return false;
}

return $existsInCache;
// single read instead of has()+get(): an entry expiring between the two
// calls would assign null to the typed $response property (TypeError)
$miss = new \stdClass;
$cached = Cache::get($this->getCacheKey(), $miss);

if ($cached === $miss || (!is_array($cached) && !is_object($cached))) {
return false;
}

$this->response = $cached;

return true;
}

protected function loadTokenFromCache(): bool
Expand Down Expand Up @@ -306,11 +329,36 @@ protected function setResponse(Response $response): self
$this
->restoreRequestEncoding()
->checkResponse($response)
->response = json_decode($response->body());
->response = $this->decodeResponseBody($response);

return $this->cacheResponse();
}

protected function decodeResponseBody(Response $response): array|object
{
$body = $response->body();

if (trim($body) === '') {
return new \stdClass;
}

$decoded = json_decode($body);

if (json_last_error() !== JSON_ERROR_NONE) {
// previously this assigned the failed decode result (null) to the
// typed $response property and crashed with an opaque TypeError
throw new Exception('response body is not valid JSON: '.json_last_error_msg());
}

if (!is_array($decoded) && !is_object($decoded)) {
// JSON scalar bodies (e.g. a bare string from an RPC-style action)
// are wrapped so the array|object contract keeps holding
return (object) ['value' => $decoded];
}

return $decoded;
}

protected function setTemporaryRequestEncoding(?string $encoding): self
{
$this->requestEncodingCache = $this->requestEncoding;
Expand Down