Skip to content

Commit

Permalink
Merge pull request #210 from cultuurnet/III-4788-api-problem-types
Browse files Browse the repository at this point in the history
III-4788 Change error responses to return correct API problems
  • Loading branch information
bertramakers committed Jun 23, 2022
2 parents 0426c8c + 84e8207 commit 7bd25c9
Show file tree
Hide file tree
Showing 14 changed files with 158 additions and 40 deletions.
21 changes: 20 additions & 1 deletion app/Error/ApiExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
namespace CultuurNet\UDB3\SearchService\Error;

use Crell\ApiProblem\ApiProblem;
use CultuurNet\UDB3\Search\ConvertsToApiProblem;
use CultuurNet\UDB3\Search\Http\ResponseFactory;
use CultuurNet\UDB3\Search\Json;
use Elasticsearch\Common\Exceptions\ElasticsearchException;
use Error;
use Fig\Http\Message\StatusCodeInterface;
use League\Route\Http\Exception\MethodNotAllowedException;
use League\Route\Http\Exception\NotFoundException;
use Whoops\Handler\Handler;
use Zend\HttpHandlerRunner\Emitter\EmitterInterface;

Expand Down Expand Up @@ -39,7 +42,7 @@ public function handle(): ?int
$problem = $this->createNewApiProblem($jsonSerializableException);

$this->emitter->emit(
ResponseFactory::jsonLd(
ResponseFactory::apiProblem(
$problem->asArray(),
$problem->getStatus()
)
Expand All @@ -55,6 +58,22 @@ private function createNewApiProblem(\Throwable $throwable): ApiProblem
->setStatus(StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
}

if ($throwable instanceof ConvertsToApiProblem) {
return $throwable->convertToApiProblem();
}

if ($throwable instanceof NotFoundException) {
$problem = new ApiProblem('Not Found', 'https://api.publiq.be/probs/url/not-found');
$problem->setStatus(404);
return $problem;
}

if ($throwable instanceof MethodNotAllowedException) {
$problem = new ApiProblem('Method not allowed', 'https://api.publiq.be/probs/method/not-allowed');
$problem->setStatus(405);
return $problem;
}

$problem = new ApiProblem($throwable->getMessage());
$problem->setStatus($throwable->getCode() ?: StatusCodeInterface::STATUS_BAD_REQUEST);
return $problem;
Expand Down
34 changes: 24 additions & 10 deletions app/RoutingServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,38 @@ function () {
)
);

// Register the OPTIONS method for every route to make the CORS middleware registered above work.
$router->options('/{path:.*}', static function () {
return new Response(StatusCodeInterface::STATUS_NO_CONTENT);
});
$optionsResponse = static fn () => new Response(StatusCodeInterface::STATUS_NO_CONTENT);

// Register the OPTIONS method for every route to make the CORS middleware registered above work.
$router->get('/organizers', OrganizerSearchController::class);
$router->get('/offers', ['offer_controller', '__invoke']);
$router->get('/events', ['event_controller', '__invoke']);
$router->get('/places', ['place_controller', '__invoke']);
$router->get('/event', ['event_controller', '__invoke']);
$router->get('/place', ['place_controller', '__invoke']);

$router->options('/organizers', $optionsResponse);
$router->get('/organizers/', OrganizerSearchController::class);
$router->options('/organizers/', $optionsResponse);

$router->get('/offers', ['offer_controller', '__invoke']);
$router->options('/offers', $optionsResponse);
$router->get('/offers/', ['offer_controller', '__invoke']);
$router->options('/offers/', $optionsResponse);

$router->get('/events', ['event_controller', '__invoke']);
$router->options('/events', $optionsResponse);
$router->get('/events/', ['event_controller', '__invoke']);
$router->options('/events/', $optionsResponse);

$router->get('/places', ['place_controller', '__invoke']);
$router->options('/places', $optionsResponse);
$router->get('/places/', ['place_controller', '__invoke']);
$router->options('/places/', $optionsResponse);

$router->get('/event', ['event_controller', '__invoke']);
$router->options('/event', $optionsResponse);
$router->get('/event/', ['event_controller', '__invoke']);
$router->options('/event/', $optionsResponse);

$router->get('/place', ['place_controller', '__invoke']);
$router->options('/place', $optionsResponse);
$router->get('/place/', ['place_controller', '__invoke']);
$router->options('/place/', $optionsResponse);

return $router;
}
Expand Down
22 changes: 22 additions & 0 deletions src/AbstractQueryParameterException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace CultuurNet\UDB3\Search;

use Crell\ApiProblem\ApiProblem;
use InvalidArgumentException;

abstract class AbstractQueryParameterException extends InvalidArgumentException implements ConvertsToApiProblem
{
public function convertToApiProblem(): ApiProblem
{
$problem = new ApiProblem(
'Not Found',
'https://api.publiq.be/probs/url/not-found'
);
$problem->setStatus(404);
$problem->setDetail($this->message);
return $problem;
}
}
12 changes: 12 additions & 0 deletions src/ConvertsToApiProblem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace CultuurNet\UDB3\Search;

use Crell\ApiProblem\ApiProblem;

interface ConvertsToApiProblem
{
public function convertToApiProblem(): ApiProblem;
}
17 changes: 14 additions & 3 deletions src/ElasticSearch/Organizer/ElasticSearchOrganizerQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,20 @@ public function withAutoCompleteFilter(string $input): ElasticSearchOrganizerQue
return $this->withMatchPhraseQuery('name.nl.autocomplete', $input);
}

public function withWebsiteFilter(Url $url): ElasticSearchOrganizerQueryBuilder
{
return $this->withMatchQuery('url', $url->getNormalizedUrl());
public function withWebsiteFilter(string $url): ElasticSearchOrganizerQueryBuilder
{
// Try to normalize the URL to return as many relevant results as possible.
// If the URL could not be parsed, use it as given. This may result in 0 results if the URL is really invalid,
// but that is logical since no organizers have that URL then. And in the case that an (older) organizer has
// an invalid URL, this still makes it possible to look it up.
try {
$urlObject = new Url($url);
$normalizedUrl = $urlObject->getNormalizedUrl();
} catch (\InvalidArgumentException $e) {
$normalizedUrl = $url;
}

return $this->withMatchQuery('url', $normalizedUrl);
}

public function withDomainFilter(string $domain): ElasticSearchOrganizerQueryBuilder
Expand Down
5 changes: 1 addition & 4 deletions src/Http/OrganizerSearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use CultuurNet\UDB3\Search\Address\PostalCode;
use CultuurNet\UDB3\Search\Creator;
use CultuurNet\UDB3\Search\ElasticSearch\JsonDocument\Properties\Url;
use CultuurNet\UDB3\Search\ElasticSearch\Organizer\ElasticSearchOrganizerQueryBuilder;
use CultuurNet\UDB3\Search\Http\Authentication\Consumer;
use CultuurNet\UDB3\Search\Http\Organizer\RequestParser\OrganizerRequestParser;
Expand Down Expand Up @@ -101,9 +100,7 @@ public function __invoke(ApiRequestInterface $request): ResponseInterface
}

if ($request->hasQueryParam('website')) {
$queryBuilder = $queryBuilder->withWebsiteFilter(
new Url($request->getQueryParam('website'))
);
$queryBuilder = $queryBuilder->withWebsiteFilter($request->getQueryParam('website'));
}

if ($request->hasQueryParam('domain')) {
Expand Down
18 changes: 17 additions & 1 deletion src/Http/ResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,25 @@ final class ResponseFactory
*/
public static function jsonLd($data, int $code = StatusCodeInterface::STATUS_OK): ResponseInterface
{
return self::jsonWithCustomContentType('application/ld+json', $data, $code);
}

/**
* @param array|object $data
*/
public static function apiProblem($data, int $code = StatusCodeInterface::STATUS_OK): ResponseInterface
{
return self::jsonWithCustomContentType('application/problem+json', $data, $code);
}

private static function jsonWithCustomContentType(
string $contentType,
$data,
int $code = StatusCodeInterface::STATUS_OK
): ResponseInterface {
$response = new Response($code);

$response = $response->withAddedHeader('Content-Type', 'application/ld+json');
$response = $response->withAddedHeader('Content-Type', $contentType);

$body = $response->getBody();
$body->write(Json::encodeWithOptions($data, self::JSON_OPTIONS));
Expand Down
4 changes: 1 addition & 3 deletions src/MissingParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace CultuurNet\UDB3\Search;

use InvalidArgumentException;

final class MissingParameter extends InvalidArgumentException
final class MissingParameter extends AbstractQueryParameterException
{
}
3 changes: 1 addition & 2 deletions src/Organizer/OrganizerQueryBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use CultuurNet\UDB3\Search\Address\PostalCode;
use CultuurNet\UDB3\Search\Country;
use CultuurNet\UDB3\Search\Creator;
use CultuurNet\UDB3\Search\ElasticSearch\JsonDocument\Properties\Url;
use CultuurNet\UDB3\Search\GeoBoundsParameters;
use CultuurNet\UDB3\Search\GeoDistanceParameters;
use CultuurNet\UDB3\Search\Label\LabelName;
Expand All @@ -20,7 +19,7 @@ interface OrganizerQueryBuilderInterface extends QueryBuilder
{
public function withAutoCompleteFilter(string $input): OrganizerQueryBuilderInterface;

public function withWebsiteFilter(Url $url): OrganizerQueryBuilderInterface;
public function withWebsiteFilter(string $url): OrganizerQueryBuilderInterface;

public function withDomainFilter(string $domain): OrganizerQueryBuilderInterface;

Expand Down
4 changes: 1 addition & 3 deletions src/UnsupportedParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace CultuurNet\UDB3\Search;

use InvalidArgumentException;

final class UnsupportedParameter extends InvalidArgumentException
final class UnsupportedParameter extends AbstractQueryParameterException
{
}
4 changes: 1 addition & 3 deletions src/UnsupportedParameterValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace CultuurNet\UDB3\Search;

use InvalidArgumentException;

final class UnsupportedParameterValue extends InvalidArgumentException
final class UnsupportedParameterValue extends AbstractQueryParameterException
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use CultuurNet\UDB3\Search\Creator;
use CultuurNet\UDB3\Search\ElasticSearch\AbstractElasticSearchQueryBuilderTest;
use CultuurNet\UDB3\Search\ElasticSearch\ElasticSearchDistance;
use CultuurNet\UDB3\Search\ElasticSearch\JsonDocument\Properties\Url;
use CultuurNet\UDB3\Search\ElasticSearch\LuceneQueryString;
use CultuurNet\UDB3\Search\GeoBoundsParameters;
use CultuurNet\UDB3\Search\Geocoding\Coordinate\Coordinates;
Expand Down Expand Up @@ -169,10 +168,10 @@ public function it_should_build_a_query_with_an_autocomplete_filter(): void
/**
* @test
*/
public function it_should_build_a_query_with_a_website_filter(): void
public function it_should_build_a_query_with_a_website_filter_and_normalize_it_if_it_is_a_valid_url(): void
{
$builder = (new ElasticSearchOrganizerQueryBuilder())
->withWebsiteFilter(new Url('http://foo.bar'));
->withWebsiteFilter('http://foo.bar');

$expectedQueryArray = [
'_source' => ['@id', '@type', 'originalEncodedJsonLd', 'regions'],
Expand Down Expand Up @@ -203,6 +202,43 @@ public function it_should_build_a_query_with_a_website_filter(): void
$this->assertEquals($expectedQueryArray, $actualQueryArray);
}

/**
* @test
*/
public function it_does_not_throw_for_an_invalid_url_as_website(): void
{
$builder = (new ElasticSearchOrganizerQueryBuilder())
->withWebsiteFilter('foobar');

$expectedQueryArray = [
'_source' => ['@id', '@type', 'originalEncodedJsonLd', 'regions'],
'from' => 0,
'size' => 30,
'query' => [
'bool' => [
'must' => [
[
'match_all' => (object) [],
],
],
'filter' => [
[
'match' => [
'url' => [
'query' => 'foobar',
],
],
],
],
],
],
];

$actualQueryArray = $builder->build();

$this->assertEquals($expectedQueryArray, $actualQueryArray);
}

/**
* @test
*/
Expand Down Expand Up @@ -282,7 +318,7 @@ public function it_should_build_a_query_with_multiple_filters(): void
->withStart(new Start(30))
->withLimit(new Limit(10))
->withAutoCompleteFilter('foo')
->withWebsiteFilter(new Url('http://foo.bar'));
->withWebsiteFilter('http://foo.bar');

$expectedQueryArray = [
'_source' => ['@id', '@type', 'originalEncodedJsonLd', 'regions'],
Expand Down Expand Up @@ -918,7 +954,7 @@ public function it_should_always_return_a_clone_for_each_mutation(): void
->withStart(new Start(30))
->withLimit(new Limit(10))
->withAutoCompleteFilter('foo')
->withWebsiteFilter(new Url('http://foo.bar'));
->withWebsiteFilter('http://foo.bar');

$expectedQueryArray = [
'_source' => ['@id', '@type', 'originalEncodedJsonLd', 'regions'],
Expand Down
5 changes: 2 additions & 3 deletions tests/Http/MockOrganizerQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use CultuurNet\UDB3\Search\Address\PostalCode;
use CultuurNet\UDB3\Search\Country;
use CultuurNet\UDB3\Search\Creator;
use CultuurNet\UDB3\Search\ElasticSearch\JsonDocument\Properties\Url;
use CultuurNet\UDB3\Search\GeoBoundsParameters;
use CultuurNet\UDB3\Search\GeoDistanceParameters;
use CultuurNet\UDB3\Search\Label\LabelName;
Expand Down Expand Up @@ -39,10 +38,10 @@ public function withAutoCompleteFilter(string $input): MockOrganizerQueryBuilder
return $c;
}

public function withWebsiteFilter(Url $url): MockOrganizerQueryBuilder
public function withWebsiteFilter(string $url): MockOrganizerQueryBuilder
{
$c = clone $this;
$c->mockQuery['website'] = $url->toString();
$c->mockQuery['website'] = $url;
return $c;
}

Expand Down
3 changes: 1 addition & 2 deletions tests/Http/OrganizerSearchControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use CultuurNet\UDB3\Search\Address\PostalCode;
use CultuurNet\UDB3\Search\Country;
use CultuurNet\UDB3\Search\Creator;
use CultuurNet\UDB3\Search\ElasticSearch\JsonDocument\Properties\Url;
use CultuurNet\UDB3\Search\Facet\FacetFilter;
use CultuurNet\UDB3\Search\Facet\FacetNode;
use CultuurNet\UDB3\Search\Geocoding\Coordinate\Coordinates;
Expand Down Expand Up @@ -122,7 +121,7 @@ public function it_returns_a_paged_collection_of_search_results_based_on_request
new Language('nl'),
new Language('en')
)
->withWebsiteFilter(new Url('http://foo.bar'))
->withWebsiteFilter('http://foo.bar')
->withDomainFilter('www.publiq.be')
->withPostalCodeFilter(new PostalCode('3000'))
->withAddressCountryFilter(new Country('NL'))
Expand Down

0 comments on commit 7bd25c9

Please sign in to comment.