* @author Tobias Nyholm */ final class GithubExceptionThrower implements Plugin { /** * @return Promise */ public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { return $next($request)->then(function (ResponseInterface $response) use ($request) { if ($response->getStatusCode() < 400 || $response->getStatusCode() > 600) { $this->checkGraphqlErrors($response); return $response; } // If error: $remaining = ResponseMediator::getHeader($response, 'X-RateLimit-Remaining'); if (null !== $remaining && 1 > $remaining && 'rate_limit' !== substr($request->getRequestTarget(), 1, 10)) { $limit = (int) ResponseMediator::getHeader($response, 'X-RateLimit-Limit'); $reset = (int) ResponseMediator::getHeader($response, 'X-RateLimit-Reset'); throw new ApiLimitExceedException($limit, $reset); } if ((401 === $response->getStatusCode()) && $response->hasHeader('X-GitHub-OTP') && 0 === strpos((string) ResponseMediator::getHeader($response, 'X-GitHub-OTP'), 'required;')) { $type = substr((string) ResponseMediator::getHeader($response, 'X-GitHub-OTP'), 9); throw new TwoFactorAuthenticationRequiredException($type); } $content = ResponseMediator::getContent($response); if (is_array($content) && isset($content['message'])) { if (400 === $response->getStatusCode()) { throw new ErrorException(sprintf('%s (%s)', $content['message'], $response->getReasonPhrase()), 400); } if (422 === $response->getStatusCode() && isset($content['errors'])) { $errors = []; foreach ($content['errors'] as $error) { switch ($error['code']) { case 'missing': $errors[] = sprintf('The %s %s does not exist, for resource "%s"', $error['field'], $error['value'], $error['resource']); break; case 'missing_field': $errors[] = sprintf('Field "%s" is missing, for resource "%s"', $error['field'], $error['resource']); break; case 'invalid': if (isset($error['message'])) { $errors[] = sprintf('Field "%s" is invalid, for resource "%s": "%s"', $error['field'], $error['resource'], $error['message']); } else { $errors[] = sprintf('Field "%s" is invalid, for resource "%s"', $error['field'], $error['resource']); } break; case 'already_exists': $errors[] = sprintf('Field "%s" already exists, for resource "%s"', $error['field'], $error['resource']); break; default: if (isset($error['message'])) { $errors[] = $error['message']; } break; } } throw new ValidationFailedException( $errors ? 'Validation Failed: '.implode(', ', $errors) : 'Validation Failed', 422 ); } } if (502 == $response->getStatusCode() && isset($content['errors']) && is_array($content['errors'])) { $errors = []; foreach ($content['errors'] as $error) { if (isset($error['message'])) { $errors[] = $error['message']; } } throw new RuntimeException(implode(', ', $errors), 502); } if ((403 === $response->getStatusCode()) && $response->hasHeader('X-GitHub-SSO') && 0 === strpos((string) ResponseMediator::getHeader($response, 'X-GitHub-SSO'), 'required;')) { // The header will look something like this: // required; url=https://github.com/orgs/octodocs-test/sso?authorization_request=AZSCKtL4U8yX1H3sCQIVnVgmjmon5fWxks5YrqhJgah0b2tlbl9pZM4EuMz4 // So we strip out the first 14 characters, leaving only the URL. // @see https://developer.github.com/v3/auth/#authenticating-for-saml-sso $url = substr((string) ResponseMediator::getHeader($response, 'X-GitHub-SSO'), 14); throw new SsoRequiredException($url); } throw new RuntimeException(isset($content['message']) ? $content['message'] : $content, $response->getStatusCode()); }); } /** * The graphql api doesn't return a 5xx http status for errors. Instead it returns a 200 with an error body. * * @throws RuntimeException */ private function checkGraphqlErrors(ResponseInterface $response): void { if ($response->getStatusCode() !== 200) { return; } $content = ResponseMediator::getContent($response); if (!is_array($content)) { return; } if (!isset($content['errors']) || !is_array($content['errors'])) { return; } $errors = []; foreach ($content['errors'] as $error) { if (isset($error['message'])) { $errors[] = $error['message']; } } if (empty($errors)) { return; } throw new RuntimeException(implode(', ', $errors)); } }