Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce RequestValidationError and ResponseValidationError exceptions #26

Merged
merged 7 commits into from
Jul 31, 2019

Conversation

gweis
Copy link
Contributor

@gweis gweis commented Jun 10, 2019

remove add_validation_error_view config directive which can be replaced by standard pyramid custom exception views
re-raise any exceptions thrown in view (to keep view semantics of returning vs throwing HTTPExceptions)
updated tests

remove add_validation_error_view config directive which can be replaced by standard pyramid custom exception views
re-raise any exceptions thrown in view (to keep view semantics of returning vs throwing HTTPExceptions)
updated tests
@gweis
Copy link
Contributor Author

gweis commented Jun 10, 2019

Seems like I overlooked that circle-ci is running the examples. I'll update

Copy link
Collaborator

@zupo zupo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial quick look: I like the PR a lot. But please give me a few days to test it out in our big-ish private codebase and also on https://github.com/niteoweb/pyramid-realworld-example-app, to make sure all edge cases are covered.

pyramid_openapi3/tests/test_validation.py Outdated Show resolved Hide resolved
…nge / render a response like pyramid's default exception view have a chance to do their work and the final response then get's validated

split out code base into smaller modules (tween, exceptions, etc..)
fix example applications
@gweis
Copy link
Contributor Author

gweis commented Jun 10, 2019

I have updated the pull request and changed the behaviour a fair bit.
The idea is, that request validation happens just like before, just that RequestValidationError which is a subclass of HTTPException get's thrown. A side effect of this is, that standard pyramid exception handling can easily handle it, but pretty much any operation in openapi.yaml now needs to define a 400 response code to correctly validate a possible an invalid request response.

I have moved the response validation code into a tween, so that other tweens like pyramid_exception_tween to handle the exception and generate a response before response validation kicks in.

There are probably still a few rough edges, but now it covers pretty much all my use cases.
(Sorry the pull request got a bit larger than expected)

@zupo
Copy link
Collaborator

zupo commented Jun 10, 2019

Great, I'll have a look!

but pretty much any operation in openapi.yaml now needs to define a 400 response code to correctly validate a possible an invalid request response.

This is fine, I'm planning to provide a way to enforce common responses soon-ish.

pyramid_openapi3/tween.py Outdated Show resolved Hide resolved
@zupo
Copy link
Collaborator

zupo commented Jun 10, 2019

No changes required on pyramid-realworld-example-app: https://travis-ci.com/niteoweb/pyramid-realworld-example-app/builds/115003852

@gweis
Copy link
Contributor Author

gweis commented Jun 10, 2019

No changes required on pyramid-realworld-example-app

That's great. The only changes that may be needed are replacing config.add_validation_error_view with config.add_exception_view and maybe adding '400' responses to the projects openapi.yaml.

In a future pull/feature request, I'd like to make the request/response validation optional. E.g.: by configuring the view with a openapi=(True, False) # enable request, disable response validation. Not sure yet, but I may have some rare use cases for that, and it would be a very minimal change to support that.

@zupo
Copy link
Collaborator

zupo commented Jun 11, 2019

by configuring the view with a openapi=(True, False)

As long as openapi stays enough for the default behaviour (no need to say (True, True), then +1

@zupo
Copy link
Collaborator

zupo commented Jun 11, 2019

Still need to test this against our private codebase that is currently the biggest user of pyramid_openapi3.

@gweis
Copy link
Contributor Author

gweis commented Jun 11, 2019

Still need to test this against our private codebase that is currently the biggest user of pyramid_openapi3

Yes, that is the intention. No backwards incompatible changes necessary. (and barely a performance hit, as this would be parsed on startup).

@MatthewWilkes
Copy link
Contributor

I've gone over the changes here and it looks really good to me. My only concern was a minor readability item in the tests.

def __init__(self, *args, errors, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.errors = errors
self.detail = self.message = "\n".join(str(e) for e in errors)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be deferred to handler

Copy link
Contributor Author

@gweis gweis Jun 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the exception view handler? The pyramid default exception view handler does not handle exceptions with an error attribute (or any other custom attribute for that matter). Are you suggesting to add a default exception handler to this library?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the exact line self.detail = self.message = "\n".join(str(e) for e in errors) you are creating object in memory that are not even needed yet.

Copy link
Contributor Author

@gweis gweis Jun 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to keep the pyramid httpexception contract up here with some useful information from validation errors, and not re-implementing all the templating that's implemented in pyramid http exception/response.

Because the validation exceptions are subclasses of pyramids HTTPException class, the validation details can't be put into the message or detail easily. They are instance fields set in the base class __init__ method. The only way I can see to delay the generation of an error message is by turning the comment field into @property ro even @refiy descriptor. Happy to change that, but I think detail / message are more appropriate fields for that.

# If there is no exception view, we also see request validation errors here
except ResponseValidationError:
exc_info = sys.exc_info()
try:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can reuse _error_handler

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whether it is good practice to re-use private functions?

Copy link

@dz0ny dz0ny Jun 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've run it on an internal project and it seems it breaks badly because exception handling is reimplemented. Will have more info in a couple of days. It's specific to us because we enforce response spec (with some additional info).

The second one is debug_toolbar:

Traceback (most recent call last):
  File ".venv/lib/python3.7/site-packages/pyramid_debugtoolbar/toolbar.py", line 258, in toolbar_tween
    toolbar.status_int = response.status_int
AttributeError: 'ResponseValidationResult' object has no attribute 'status_int'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there is somewhere a custom exception view handler that returns the ResponseValidationResult instead of a Response / HTTPException instance? Otherwise I can't see why there would be a ResponseValidationResult returned by the debug_toolbar tween handler.

@zupo
Copy link
Collaborator

zupo commented Jun 22, 2019

@gweis: quick update: got the private project's build green, now need to get it through review and pushed to production.

def excview_tween(request) -> Response:
try:
response = handler(request)
if not request.environ.get("pyramid_openapi3.validate_response"):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pyramid_openapi3.validate_response infers that only response will not be validated, but the code actually ignores the request too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a bit of a source for confusion here... the Request is validated in the view wrapper, but the response is validated after the exception view handler ran (if there is any)

@dz0ny
Copy link

dz0ny commented Jun 23, 2019

@gweis Can you explain a bit what were your motivations for re-implementing error handler and not having just standard tween? I don't understand why is this better?

@gweis
Copy link
Contributor Author

gweis commented Jun 23, 2019

@dzony The original problem was, that within pyramid apps where not every view is an openapi3 view, You need some way of deciding when to validate the request. That's done in this pkg via the view wrapper.
However I then noticed, that when I throw exceptions in my openapi enabled view these are caught by the same wrapper and returned as response, meaning that I would have now two ways to handle exceptions. One for normal views, where a raised Exception "bubbles" up into the pyramid exception view handler, and raised Exceptions in an openapi view, where any exception I raise, get's returned and does not invoke the exception view handler.
So I was looking for a way to keep usual pyramid behaviour with raised or returned HTTPExceptions, use pyramids exception view handling mechanisms to handle errors, and still validate request / response / properly. This meant it was necessary to move the response validation after error view handler have been processed.
In short the aim is to align as much as possible with documented behaviour of the pyramid request chain, and re-using mechanism that already exist in pyramid instead of changing the request/response processing (esp. view error handling, regarding raised/returned exceptions).

There is also some discussion in #15

if result.errors:
raise ResponseValidationError(errors=result.errors)
# If there is no exception view, we also see request validation errors here
except ResponseValidationError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a test is missing here for if not request.environ.get('pyramid_openapi3.validate_response') incase the handler raises one of these errors for some reason on a view that's not protected by the spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure whether I follow here, but I have added another test. 48b8049

Otherwise I think a non openapi view should not raise a ResponseValidationError. Maybe we should think about that in case there is a real use case for it, otherwise I'd consider throwing random ResponseValidationError instances in user code as a bug.

try:
response = request.invoke_exception_view(exc_info)
except HTTPNotFound:
reraise(*exc_info)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a finally is missing here to del exc_info to avoid cycles on exc_info stack frames referencing the current stack frame... I will say I'm sensitive to this because of python 2 and I'm not positive it's an issue in python 3 - but I wanted to bring it up to make sure someone double checks :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a del exc_info is needed here, but I am not really sure, so I have added it anyway (can't hurt ).

make sure we don't keep exc_info around
enable python 3.6 support via pipenv
@gweis
Copy link
Contributor Author

gweis commented Jul 24, 2019

Had to update version pins for Pipfile, otherwise it would not build on travis.

@zupo zupo merged commit 64dda91 into Pylons:master Jul 31, 2019
@zupo
Copy link
Collaborator

zupo commented Jul 31, 2019

@gweis: I finally found enough time to give this PR another rundown and test it in our private codebase. It seems to work exactly as advertised , so I merged it. I'm terribly sorry it took me this long! Thanks a million for your contribution, the package is so much better off with this change!

@zupo
Copy link
Collaborator

zupo commented Aug 5, 2019

I've cut a new release that includes this PR: https://pypi.org/project/pyramid-openapi3/0.4.0/

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.

5 participants