def create(self, params, meta, **kwargs): consumer_id = kwargs['validated']['consumer_id'] service_catalog_code = kwargs['validated']['service_catalog_code'] project_id = kwargs['validated']['project_id'] account_id = kwargs['token']['account_id'] service = self.db.get(model=Service, account_id=account_id, project_id=project_id, service_catalog_code=service_catalog_code) if not service: raise HTTPBadRequest( description="Please check your service parameters! " "There is no service identified with your parameters.") elif not self.db.get_attached_consumer(consumer_id=consumer_id, project_id=project_id): raise HTTPBadRequest( description= "There is no such consumer attached to specified project." "Please check your parameters or try to attach consumer first!" ) elif not service.is_item_limit_available(): raise HTTPPaymentRequired( description="Please check your service message limit") else: token = generate_token() add_consumer_token(account_id, project_id, service_catalog_code, consumer_id, token) return {"token": token}
def on_get(self, req, resp): user = req.context["user"] if not user: raise HTTPBadRequest() if not user.api_enabled: resp.append_header("Refresh", "5;url=/profile") return self.render_template( req, resp, "message_gate.html", gate_message=Message( "danger", "No access", "You don't have API access - please contact a member of staff if you need it." ), redirect_uri="/") db_session = req.context["db_session"] keys = {} for key in db_session.query(APIKey).filter_by(user_id=user.id).all(): keys[key.key] = key.name self.render_template(req, resp, "users/api_keys.html", user=user, keys=keys)
def retrieve(self, params, meta, **kwargs): project_id = kwargs['project_id'] account_id = kwargs['token']['account_id'] admin_id = kwargs['token']['sub'] project = self.db.get_project(project_id=project_id, account_id=account_id) if not project: raise HTTPBadRequest(description="Project not found") token_payload = {'admin_id': admin_id, 'account_id': account_id, 'project_id': project_id} old_token = get_project_services_admin_token(project_id) if old_token: del_project_services_admin_token(old_token) token = generate_user_token() set_project_services_admin_token(project_id, token) add_project_services_admin_token(token, token_payload) return { "project_services_admin_token": token }
def on_put(self, req, resp, test_id): resp.status = falcon.HTTP_204 payload = json.loads(req.stream.read()) if '_id' not in payload or not isinstance(payload.get('_id'), str): raise HTTPBadRequest() try: update_resource(test_id, payload) except StopIteration: raise HTTPNotFound()
def json(self): if not hasattr(self, "_json"): if not self.client_accepts_json: raise falcon.HTTPUnsupportedMediaType( 'This API only supports the JSON formated data') try: self._json = json.loads(self.stream.read().decode('utf8')) except json.decoder.JSONDecodeError as err: raise HTTPBadRequest("JSONDecodeError", str(err)) return self._json
def _do(self): try: login_credentials = self.current['request']['context']['data']['login_crd'] except KeyError: raise HTTPBadRequest("Missing login data") user = authenticate(login_credentials) is_login_successful = bool(user) if is_login_successful: self.current.request.context['result'] = {'success': True} self.current.request.session['user'] = user self.current['task'].data['is_login_successful'] = is_login_successful
def on_post(self, req, resp): file = req.get_param('file') try: image = file.file except AttributeError: raise HTTPBadRequest('Bad multipart form', 'File is not valid') raw_response = recognize(image) response = serializer(raw_response) resp.body = response
def remove_consumer_token(project_id, service_catalog_code, token): token_key = CACHE_REFRESH_TOKEN.format(project_id=project_id, service=service_catalog_code, token=token) t = cache.hget(CACHE_TOKENS_KEYS, token) if token_key != t.decode(): raise HTTPBadRequest(description="Token does not found!.") cache.hdel(CACHE_TOKENS_KEYS, token) cache.delete(token_key)
def on_get(self, req, resp): user = req.context["user"] if not user: raise HTTPBadRequest() self.render_template( req, resp, "users/profile.html", user=user, avatar="https://www.gravatar.com/avatar/{}".format( self.gravatar_hash(user.email)))
def on_get(self, req, resp): path = req.get_param('path') if not path: description = "Specify a valid `path` querystring" raise HTTPBadRequest(title="Bad Request", description=description) cassandra_driver = CassandraStorageDriver() cassandra_driver.connect(conf['cassandra']['keyspace']) current_time = time.time() query_time = current_time - conf['canary']['interval'] job_details = \ cassandra_driver.get_job_details(date=canonicalize(query_time), path=path) cassandra_driver.close_connection() resp.status = falcon.HTTP_200 for i in range(len(job_details)): job_details[i]['jobs'] = json.loads( json.loads(job_details[i]['jobs'])) resp.body = json.dumps(job_details)
def process_request(self, req, resp): # type: (Request, Response) -> None """Ensure that a request does not contain an empty body If a request has content length, but its body is empty, raise an HTTPBadRequest error. :param req: the passed request object :param resp: the passed response object :raises HTTPBadRequest: if the request has content length with an empty body """ log.debug("EmptyRequestDropper.process_request(%s, %s)", req, resp) if req.content_length in (None, 0): return content = get_stashed_content(req) # If the content is _still_ Falsy (e.g., something empty like b'') if not content: raise HTTPBadRequest(description=( "Empty response body. A valid JSON document is required."))
def on_get(self, req, resp): path = req.get_param('path') if not path: description = "Specify a valid `path` querystring" raise HTTPBadRequest(title="Bad Request", description=description) redis_driver = redisdriver.RedisStorageDriver() job_details = redis_driver.get_job_details() resp.status = falcon.HTTP_200 for i in range(len(job_details)): flow_status = [] job_details[i] = json.loads(base64.b64decode(job_details[i])) for val in job_details[i]['jobs']: stat = {} sub_tasks = val['flow_status'].values()[0]['tasks'] for sub_val in sub_tasks: if sub_val: for task_name, task_values in sub_val.items(): stat[task_name] = task_values['state'] flow_status.append(stat) job_details[i]['flow_status'] = flow_status resp.body = json.dumps(job_details)
def bad_request(req, resp, **kwargs): """Raise 400 HTTPBadRequest error""" raise HTTPBadRequest('Bad request', 'Invalid HTTP method')
def __call__(self, env, start_response): # noqa: C901 """WSGI `app` method. Makes instances of App callable from a WSGI server. May be used to host an App or called directly in order to simulate requests when testing the App. (See also: PEP 3333) Args: env (dict): A WSGI environment dictionary start_response (callable): A WSGI helper function for setting status and headers on a response. """ req = self._request_type(env, options=self.req_options) resp = self._response_type(options=self.resp_options) resource = None responder = None params = {} dependent_mw_resp_stack = [] mw_req_stack, mw_rsrc_stack, mw_resp_stack = self._middleware req_succeeded = False try: if req.method in self._META_METHODS: raise HTTPBadRequest() # NOTE(ealogar): The execution of request middleware # should be before routing. This will allow request mw # to modify the path. # NOTE: if flag set to use independent middleware, execute # request middleware independently. Otherwise, only queue # response middleware after request middleware succeeds. if self._independent_middleware: for process_request in mw_req_stack: process_request(req, resp) if resp.complete: break else: for process_request, process_response in mw_req_stack: if process_request and not resp.complete: process_request(req, resp) if process_response: dependent_mw_resp_stack.insert(0, process_response) if not resp.complete: # NOTE(warsaw): Moved this to inside the try except # because it is possible when using object-based # traversal for _get_responder() to fail. An example is # a case where an object does not have the requested # next-hop child resource. In that case, the object # being asked to dispatch to its child will raise an # HTTP exception signalling the problem, e.g. a 404. responder, params, resource, req.uri_template = self._get_responder( req) except Exception as ex: if not self._handle_exception(req, resp, ex, params): raise else: try: # NOTE(kgriffs): If the request did not match any # route, a default responder is returned and the # resource is None. In that case, we skip the # resource middleware methods. Resource will also be # None when a middleware method already set # resp.complete to True. if resource: # Call process_resource middleware methods. for process_resource in mw_rsrc_stack: process_resource(req, resp, resource, params) if resp.complete: break if not resp.complete: responder(req, resp, **params) req_succeeded = True except Exception as ex: if not self._handle_exception(req, resp, ex, params): raise # Call process_response middleware methods. for process_response in mw_resp_stack or dependent_mw_resp_stack: try: process_response(req, resp, resource, req_succeeded) except Exception as ex: if not self._handle_exception(req, resp, ex, params): raise req_succeeded = False body = [] length = 0 try: body, length = self._get_body(resp, env.get('wsgi.file_wrapper')) except Exception as ex: if not self._handle_exception(req, resp, ex, params): raise req_succeeded = False resp_status = code_to_http_status(resp.status) default_media_type = self.resp_options.default_media_type if req.method == 'HEAD' or resp_status in _BODILESS_STATUS_CODES: body = [] # PERF(vytas): move check for the less common and much faster path # of resp_status being in {204, 304} here; NB: this builds on the # assumption _TYPELESS_STATUS_CODES <= _BODILESS_STATUS_CODES. # NOTE(kgriffs): Based on wsgiref.validate's interpretation of # RFC 2616, as commented in that module's source code. The # presence of the Content-Length header is not similarly # enforced. if resp_status in _TYPELESS_STATUS_CODES: default_media_type = None elif (length is not None and req.method == 'HEAD' and resp_status not in _BODILESS_STATUS_CODES and 'content-length' not in resp._headers): # NOTE(kgriffs): We really should be returning a Content-Length # in this case according to my reading of the RFCs. By # optionally using len(data) we let a resource simulate HEAD # by turning around and calling it's own on_get(). resp._headers['content-length'] = str(length) else: # PERF(kgriffs): Böse mußt sein. Operate directly on resp._headers # to reduce overhead since this is a hot/critical code path. # NOTE(kgriffs): We always set content-length to match the # body bytes length, even if content-length is already set. The # reason being that web servers and LBs behave unpredictably # when the header doesn't match the body (sometimes choosing to # drop the HTTP connection prematurely, for example). if length is not None: resp._headers['content-length'] = str(length) headers = resp._wsgi_headers(default_media_type) # Return the response per the WSGI spec. start_response(resp_status, headers) return body
def on_post(self, req, resp): params = {} user = req.context["user"] db_session = req.context["db_session"] keys = {} if not user: raise HTTPBadRequest() if not user.api_enabled: resp.append_header("Refresh", "5;url=/profile") return self.render_template( req, resp, "message_gate.html", gate_message=Message( "danger", "No access", "You don't have API access - please contact a member of staff if you need it." ), redirect_uri="/") for key in db_session.query(APIKey).filter_by(user_id=user.id).all(): keys[key.key] = key.name if not req.get_param("action", store=params): raise HTTPBadRequest() if params["action"] == "create": if len(keys) >= 10: return self.render_template( req, resp, "users/api_keys.html", user=user, keys=keys, message=Message( "danger", "Too many keys", "You already have 10 api keys! If you need more, contact staff directly." )) if not req.get_param("name", store=params) or not params["name"]: return self.render_template( req, resp, "users/api_keys.html", user=user, keys=keys, message=Message( "danger", "Missing name", "Please supply a name for your new API key")) new_key = APIKey(user=user, name=params["name"], key=secrets.token_urlsafe(32)) db_session.add(new_key) keys[new_key.key] = new_key.name return self.render_template( req, resp, "users/api_keys.html", user=user, keys=keys, message=Message( "success", "Key created", "New API key \"{}\" created successfully.".format( new_key.name))) elif params["action"] == "delete": if not req.get_param("key", store=params): raise HTTPBadRequest() if params["key"] not in keys: return self.render_template( req, resp, "users/api_keys.html", user=user, keys=keys, message=Message("danger", "No such key", "Unknown key: {}.".format(params["key"]))) old_key = db_session.query(APIKey).filter_by( key=params["key"]).one() db_session.delete(old_key) del keys[params["key"]] return self.render_template( req, resp, "users/api_keys.html", user=user, keys=keys, message=Message( "success", "Key deleted", "API key \"{}\" deleted successfully.".format( old_key.name))) else: raise HTTPBadRequest()
def process_resource(self, req, resp, resource, params): # type: (Request, Response, object, dict) -> None """Deserialize request body with any resource-specific schemas Store deserialized data on the ``req.context`` object under the ``req_key`` provided to the class constructor or on the ``json`` key if none was provided. If a Marshmallow schema is defined on the passed ``resource``, use it to deserialize the request body. If no schema is defined and the class was instantiated with ``force_json=True``, request data will be deserialized with any ``json_module`` passed to the class constructor or ``simplejson`` by default. :param falcon.Request req: the request object :param falcon.Response resp: the response object :param object resource: the resource object :param dict params: any parameters parsed from the url :rtype: None :raises falcon.HTTPBadRequest: if the data cannot be deserialized or decoded """ log.debug('Marshmallow.process_resource(%s, %s, %s, %s)', req, resp, resource, params) if req.content_length in (None, 0): return sch = self._get_schema(resource, req.method) if sch is not None: if not isinstance(sch, Schema): raise TypeError( 'The schema and <method>_schema properties of a resource ' 'must be instantiated Marshmallow schemas.') try: body = get_stashed_content(req).decode('utf-8') parsed = self._json.loads(body) except UnicodeDecodeError: raise HTTPBadRequest('Body was not encoded as UTF-8') except self._json.JSONDecodeError: raise HTTPBadRequest('Request must be valid JSON') data, errors = sch.load(parsed) if errors: raise HTTPUnprocessableEntity( description=self._json.dumps(errors)) req.context[self._req_key] = data elif self._force_json: body = get_stashed_content(req) try: req.context[self._req_key] = self._json.loads( body.decode('utf-8')) except (ValueError, UnicodeDecodeError): raise HTTPBadRequest(description=( 'Could not decode the request body, either because ' 'it was not valid JSON or because it was not encoded ' 'as UTF-8.'))
async def __call__(self, scope, receive, send): # noqa: C901 try: asgi_info = scope['asgi'] # NOTE(kgriffs): We only check this here because # uvicorn does not explicitly set the 'asgi' key, which # would normally mean we should assume '2.0', but uvicorn # actually *does* support 3.0. But in that case, we will # end up in the except clause, below, and not raise an # error. # PERF(kgriffs): This should usually be present, so use a # try..except try: version = asgi_info['version'] except KeyError: # NOTE(kgriffs): According to the ASGI spec, "2.0" is # the default version. version = '2.0' if not version.startswith('3.'): raise UnsupportedScopeError( f'Falcon requires ASGI version 3.x. Detected: {asgi_info}') except KeyError: asgi_info = scope['asgi'] = {'version': '2.0'} # NOTE(kgriffs): The ASGI spec requires the 'type' key to be present. scope_type = scope['type'] if scope_type != 'http': if scope_type == 'lifespan': try: spec_version = asgi_info['spec_version'] except KeyError: spec_version = '1.0' if not spec_version.startswith( '1.') and not spec_version.startswith('2.'): raise UnsupportedScopeError( 'Only versions 1.x and 2.x of the ASGI "lifespan" scope are supported.' ) await self._call_lifespan_handlers(spec_version, scope, receive, send) return elif scope_type == 'websocket': try: spec_version = asgi_info['spec_version'] except KeyError: spec_version = '2.0' if not spec_version.startswith('2.'): raise UnsupportedScopeError( 'Only versions 2.x of the ASGI "websocket" scope are supported.' ) try: http_version = scope['http_version'] except KeyError: http_version = '1.1' if http_version not in {'1.1', '2', '3'}: raise UnsupportedError( f'The ASGI "websocket" scope does not support HTTP version {http_version}.' ) await self._handle_websocket(spec_version, scope, receive, send) return # NOTE(kgriffs): According to the ASGI spec: "Applications should # actively reject any protocol that they do not understand with # an Exception (of any type)." raise UnsupportedScopeError( f'The ASGI "{scope_type}" scope type is not supported.') # PERF(kgriffs): This is slighter faster than using dict.get() # TODO(kgriffs): Use this to determine what features are supported by # the server (e.g., the headers key in the WebSocket Accept # response). spec_version = asgi_info[ 'spec_version'] if 'spec_version' in asgi_info else '2.0' if not spec_version.startswith('2.'): raise UnsupportedScopeError( f'The ASGI http scope version {spec_version} is not supported.' ) # NOTE(kgriffs): Per the ASGI spec, we should not proceed with request # processing until after we receive an initial 'http.request' event. first_event = await receive() first_event_type = first_event['type'] if first_event_type == EventType.HTTP_DISCONNECT: # NOTE(kgriffs): Bail out immediately to minimize resource usage return # NOTE(kgriffs): This is the only other type defined by the ASGI spec, # but we just assert it to make it easier to track down a potential # incompatibility with a future spec version. assert first_event_type == EventType.HTTP_REQUEST req = self._request_type(scope, receive, first_event=first_event, options=self.req_options) resp = self._response_type(options=self.resp_options) if self.req_options.auto_parse_form_urlencoded: raise UnsupportedError( 'The deprecated WSGI RequestOptions.auto_parse_form_urlencoded option ' 'is not supported for ASGI apps. Please use Request.get_media() instead. ' ) resource = None responder = None params = {} dependent_mw_resp_stack = [] mw_req_stack, mw_rsrc_stack, mw_resp_stack = self._middleware req_succeeded = False try: if req.method in self._META_METHODS: raise HTTPBadRequest() # NOTE(ealogar): The execution of request middleware # should be before routing. This will allow request mw # to modify the path. # NOTE: if flag set to use independent middleware, execute # request middleware independently. Otherwise, only queue # response middleware after request middleware succeeds. if self._independent_middleware: for process_request in mw_req_stack: await process_request(req, resp) if resp.complete: break else: for process_request, process_response in mw_req_stack: if process_request and not resp.complete: await process_request(req, resp) if process_response: dependent_mw_resp_stack.insert(0, process_response) if not resp.complete: # NOTE(warsaw): Moved this to inside the try except # because it is possible when using object-based # traversal for _get_responder() to fail. An example is # a case where an object does not have the requested # next-hop child resource. In that case, the object # being asked to dispatch to its child will raise an # HTTP exception signaling the problem, e.g. a 404. responder, params, resource, req.uri_template = self._get_responder( req) except Exception as ex: if not await self._handle_exception(req, resp, ex, params): raise else: try: # NOTE(kgriffs): If the request did not match any # route, a default responder is returned and the # resource is None. In that case, we skip the # resource middleware methods. Resource will also be # None when a middleware method already set # resp.complete to True. if resource: # Call process_resource middleware methods. for process_resource in mw_rsrc_stack: await process_resource(req, resp, resource, params) if resp.complete: break if not resp.complete: await responder(req, resp, **params) req_succeeded = True except Exception as ex: if not await self._handle_exception(req, resp, ex, params): raise # Call process_response middleware methods. for process_response in mw_resp_stack or dependent_mw_resp_stack: try: await process_response(req, resp, resource, req_succeeded) except Exception as ex: if not await self._handle_exception(req, resp, ex, params): raise req_succeeded = False data = b'' try: data = await resp.render_body() except Exception as ex: if not await self._handle_exception(req, resp, ex, params): raise req_succeeded = False resp_status = http_status_to_code(resp.status) default_media_type = self.resp_options.default_media_type if req.method == 'HEAD' or resp_status in _BODILESS_STATUS_CODES: # # PERF(vytas): move check for the less common and much faster path # of resp_status being in {204, 304} here; NB: this builds on the # assumption _TYPELESS_STATUS_CODES <= _BODILESS_STATUS_CODES. # # NOTE(kgriffs): Based on wsgiref.validate's interpretation of # RFC 2616, as commented in that module's source code. The # presence of the Content-Length header is not similarly # enforced. # # NOTE(kgriffs): Assuming the same for ASGI until proven otherwise. # if resp_status in _TYPELESS_STATUS_CODES: default_media_type = None elif ( # NOTE(kgriffs): If they are going to stream using an # async generator, we can't know in advance what the # content length will be. (data is not None or not resp.stream) and req.method == 'HEAD' and resp_status not in _BODILESS_STATUS_CODES and 'content-length' not in resp._headers): # NOTE(kgriffs): We really should be returning a Content-Length # in this case according to my reading of the RFCs. By # optionally using len(data) we let a resource simulate HEAD # by turning around and calling it's own on_get(). resp._headers['content-length'] = str( len(data)) if data else '0' await send({ 'type': EventType.HTTP_RESPONSE_START, 'status': resp_status, 'headers': resp._asgi_headers(default_media_type) }) await send(_EVT_RESP_EOF) self._schedule_callbacks(resp) return sse_emitter = resp.sse if sse_emitter: if isasyncgenfunction(sse_emitter): raise TypeError( 'Response.sse must be an async iterable. This can be obtained by ' 'simply executing the async generator function and then setting ' 'the result to Response.sse, e.g.: resp.sse = some_asyncgen_function()' ) # NOTE(kgriffs): This must be done in a separate task because # receive() can block for some time (until the connection is # actually closed). async def watch_disconnect(): while True: received_event = await receive() if received_event['type'] == EventType.HTTP_DISCONNECT: break watcher = falcon.create_task(watch_disconnect()) await send({ 'type': EventType.HTTP_RESPONSE_START, 'status': resp_status, 'headers': resp._asgi_headers('text/event-stream') }) self._schedule_callbacks(resp) handler, _, _ = self.resp_options.media_handlers._resolve( MEDIA_JSON, MEDIA_JSON, raise_not_found=False) # TODO(kgriffs): Do we need to do anything special to handle when # a connection is closed? async for event in sse_emitter: if not event: event = SSEvent() # NOTE(kgriffs): According to the ASGI spec, once the client # disconnects, send() acts as a no-op. We have to check # the connection state using watch_disconnect() above. await send({ 'type': EventType.HTTP_RESPONSE_BODY, 'body': event.serialize(handler), 'more_body': True }) if watcher.done(): break watcher.cancel() try: await watcher except asyncio.CancelledError: pass await send({'type': EventType.HTTP_RESPONSE_BODY}) return if data is not None: # PERF(kgriffs): Böse mußt sein. Operate directly on resp._headers # to reduce overhead since this is a hot/critical code path. # NOTE(kgriffs): We always set content-length to match the # body bytes length, even if content-length is already set. The # reason being that web servers and LBs behave unpredictably # when the header doesn't match the body (sometimes choosing to # drop the HTTP connection prematurely, for example). resp._headers['content-length'] = str(len(data)) await send({ 'type': EventType.HTTP_RESPONSE_START, 'status': resp_status, 'headers': resp._asgi_headers(default_media_type) }) await send({'type': EventType.HTTP_RESPONSE_BODY, 'body': data}) self._schedule_callbacks(resp) return stream = resp.stream if not stream: resp._headers['content-length'] = '0' await send({ 'type': EventType.HTTP_RESPONSE_START, 'status': resp_status, 'headers': resp._asgi_headers(default_media_type) }) if stream: # Detect whether this is one of the following: # # (a) async file-like object (e.g., aiofiles) # (b) async generator # (c) async iterator # if hasattr(stream, 'read'): while True: data = await stream.read(self._STREAM_BLOCK_SIZE) if data == b'': break else: await send({ 'type': EventType.HTTP_RESPONSE_BODY, # NOTE(kgriffs): Handle the case in which data == None 'body': data or b'', 'more_body': True }) else: # NOTE(kgriffs): Works for both async generators and iterators try: async for data in stream: # NOTE(kgriffs): We can not rely on StopIteration # because of Pep 479 that is implemented starting # with Python 3.7. AFAICT this is only an issue # when using an async iterator instead of an async # generator. if data is None: break await send({ 'type': EventType.HTTP_RESPONSE_BODY, 'body': data, 'more_body': True }) except TypeError as ex: if isasyncgenfunction(stream): raise TypeError( 'The object assigned to Response.stream appears to ' 'be an async generator function. A generator ' 'object is expected instead. This can be obtained ' 'simply by calling the generator function, e.g.: ' 'resp.stream = some_asyncgen_function()') raise TypeError( 'Response.stream must be a generator or implement an ' '__aiter__ method. Error raised while iterating over ' 'Response.stream: ' + str(ex)) if hasattr(stream, 'close'): await stream.close() await send(_EVT_RESP_EOF) self._schedule_callbacks(resp)
def error_invalid_parameter(error = ERR_INVALID_PARAMETER , description=None): raise HTTPBadRequest(error.get('title'),description , code = error.get('code'))
def error_invalid_header( error = ERR_INVALID_HEADER , description=None): raise HTTPBadRequest(title=error.get('title'),description = description , code = error.get('code'))
async def bad_request_async(req, resp, **kwargs): """Raise 400 HTTPBadRequest error""" raise HTTPBadRequest(title='Bad request', description='Invalid HTTP method')
def error_handler(ex, req, resp, params): if isinstance(ex, NotImplementedError): resp.status = falcon.HTTP_NOT_IMPLEMENTED else: raise HTTPBadRequest(type(ex).__name__, str(ex))
def process_resource(self, req, resp, resource, params): # type: (Request, Response, object, dict) -> None """Deserialize request body with any resource-specific schemas Store deserialized data on the ``req.context`` object under the ``req_key`` provided to the class constructor or on the ``json`` key if none was provided. If a Marshmallow schema is defined on the passed ``resource``, use it to deserialize the request body. If no schema is defined and the class was instantiated with ``force_json=True``, request data will be deserialized with any ``json_module`` passed to the class constructor or ``simplejson`` by default. :param falcon.Request req: the request object :param falcon.Response resp: the response object :param object resource: the resource object :param dict params: any parameters parsed from the url :rtype: None :raises falcon.HTTPBadRequest: if the data cannot be deserialized or decoded """ log.debug( "Marshmallow.process_resource(%s, %s, %s, %s)", req, resp, resource, params, ) if req.content_length in (None, 0): return sch = self._get_schema(resource, req.method, "request") if sch is not None: if not isinstance(sch, Schema): raise TypeError( "The schema and <method>_schema properties of a resource " "must be instantiated Marshmallow schemas.") try: body = get_stashed_content(req) parsed = self._json.loads(body) except UnicodeDecodeError: raise HTTPBadRequest("Body was not encoded as UTF-8") except self._json.JSONDecodeError: raise HTTPBadRequest("Request must be valid JSON") if MARSHMALLOW_2: data, errors = sch.load(parsed) if errors: raise HTTPUnprocessableEntity( description=self._json.dumps(errors)) else: # Marshmallow 3 or higher raises a ValidationError # instead of returning a (data, errors) tuple. try: data = sch.load(parsed) except ValidationError as exc: raise HTTPUnprocessableEntity( description=self._json.dumps(exc.messages)) except Exception as exc: raise HTTPUnprocessableEntity( description=self._json.dumps({"error": exc})) req.context[self._req_key] = data elif self._force_json: body = get_stashed_content(req) try: req.context[self._req_key] = self._json.loads(body) except (ValueError, UnicodeDecodeError): raise HTTPBadRequest(description=( "Could not decode the request body, either because " "it was not valid JSON or because it was not encoded " "as UTF-8."))
async def __call__(self, scope, receive, send): # noqa: C901 # NOTE(kgriffs): The ASGI spec requires the 'type' key to be present. scope_type = scope['type'] # PERF(kgriffs): This should usually be present, so use a # try..except try: asgi_info = scope['asgi'] except KeyError: # NOTE(kgriffs): According to the ASGI spec, "2.0" is # the default version. asgi_info = scope['asgi'] = {'version': '2.0'} try: spec_version = asgi_info['spec_version'] except KeyError: spec_version = None try: http_version = scope['http_version'] except KeyError: http_version = '1.1' spec_version = _validate_asgi_scope(scope_type, spec_version, http_version) if scope_type != 'http': # PERF(vytas): Evaluate the potentially recurring WebSocket path # first (in contrast to one-shot lifespan events). if scope_type == 'websocket': await self._handle_websocket(spec_version, scope, receive, send) return # NOTE(vytas): Else 'lifespan' -- other scope_type values have been # eliminated by _validate_asgi_scope at this point. await self._call_lifespan_handlers(spec_version, scope, receive, send) return # NOTE(kgriffs): Per the ASGI spec, we should not proceed with request # processing until after we receive an initial 'http.request' event. first_event = await receive() first_event_type = first_event['type'] # PERF(vytas): Inline the value of EventType.HTTP_DISCONNECT in this # critical code path. if first_event_type == 'http.disconnect': # NOTE(kgriffs): Bail out immediately to minimize resource usage return # NOTE(kgriffs): This is the only other type defined by the ASGI spec, # but we just assert it to make it easier to track down a potential # incompatibility with a future spec version. # PERF(vytas): Inline the value of EventType.HTTP_REQUEST in this # critical code path. assert first_event_type == 'http.request' req = self._request_type(scope, receive, first_event=first_event, options=self.req_options) resp = self._response_type(options=self.resp_options) resource = None responder = None params = {} dependent_mw_resp_stack = [] mw_req_stack, mw_rsrc_stack, mw_resp_stack = self._middleware req_succeeded = False try: if req.method in self._META_METHODS: raise HTTPBadRequest() # NOTE(ealogar): The execution of request middleware # should be before routing. This will allow request mw # to modify the path. # NOTE: if flag set to use independent middleware, execute # request middleware independently. Otherwise, only queue # response middleware after request middleware succeeds. if self._independent_middleware: for process_request in mw_req_stack: await process_request(req, resp) if resp.complete: break else: for process_request, process_response in mw_req_stack: if process_request and not resp.complete: await process_request(req, resp) if process_response: dependent_mw_resp_stack.insert(0, process_response) if not resp.complete: # NOTE(warsaw): Moved this to inside the try except # because it is possible when using object-based # traversal for _get_responder() to fail. An example is # a case where an object does not have the requested # next-hop child resource. In that case, the object # being asked to dispatch to its child will raise an # HTTP exception signaling the problem, e.g. a 404. responder, params, resource, req.uri_template = self._get_responder( req) except Exception as ex: if not await self._handle_exception(req, resp, ex, params): raise else: try: # NOTE(kgriffs): If the request did not match any # route, a default responder is returned and the # resource is None. In that case, we skip the # resource middleware methods. Resource will also be # None when a middleware method already set # resp.complete to True. if resource: # Call process_resource middleware methods. for process_resource in mw_rsrc_stack: await process_resource(req, resp, resource, params) if resp.complete: break if not resp.complete: await responder(req, resp, **params) req_succeeded = True except Exception as ex: if not await self._handle_exception(req, resp, ex, params): raise # Call process_response middleware methods. for process_response in mw_resp_stack or dependent_mw_resp_stack: try: await process_response(req, resp, resource, req_succeeded) except Exception as ex: if not await self._handle_exception(req, resp, ex, params): raise req_succeeded = False data = b'' try: # NOTE(vytas): It is only safe to inline Response.render_body() # where we can be sure it hasn't been overridden, either directly # or by modifying the behavior of its dependencies. if self._standard_response_type: # PERF(vytas): inline Response.render_body() in this critical code # path in order to shave off an await. text = resp.text if text is None: data = resp._data if data is None and resp._media is not None: # NOTE(kgriffs): We use a special _UNSET singleton since # None is ambiguous (the media handler might return None). if resp._media_rendered is _UNSET: if not resp.content_type: resp.content_type = resp.options.default_media_type handler, serialize_sync, _ = resp.options.media_handlers._resolve( resp.content_type, resp.options.default_media_type) if serialize_sync: resp._media_rendered = serialize_sync( resp._media) else: resp._media_rendered = await handler.serialize_async( resp._media, resp.content_type) data = resp._media_rendered else: try: # NOTE(kgriffs): Normally we expect text to be a string data = text.encode() except AttributeError: # NOTE(kgriffs): Assume it was a bytes object already data = text else: # NOTE(vytas): Custom reponse type. data = await resp.render_body() except Exception as ex: if not await self._handle_exception(req, resp, ex, params): raise req_succeeded = False resp_status = http_status_to_code(resp.status) default_media_type = self.resp_options.default_media_type if req.method == 'HEAD' or resp_status in _BODILESS_STATUS_CODES: # # PERF(vytas): move check for the less common and much faster path # of resp_status being in {204, 304} here; NB: this builds on the # assumption _TYPELESS_STATUS_CODES <= _BODILESS_STATUS_CODES. # # NOTE(kgriffs): Based on wsgiref.validate's interpretation of # RFC 2616, as commented in that module's source code. The # presence of the Content-Length header is not similarly # enforced. # # NOTE(kgriffs): Assuming the same for ASGI until proven otherwise. # if resp_status in _TYPELESS_STATUS_CODES: default_media_type = None elif ( # NOTE(kgriffs): If they are going to stream using an # async generator, we can't know in advance what the # content length will be. (data is not None or not resp.stream) and req.method == 'HEAD' and resp_status not in _BODILESS_STATUS_CODES and 'content-length' not in resp._headers): # NOTE(kgriffs): We really should be returning a Content-Length # in this case according to my reading of the RFCs. By # optionally using len(data) we let a resource simulate HEAD # by turning around and calling it's own on_get(). resp._headers['content-length'] = str( len(data)) if data else '0' await send({ # PERF(vytas): Inline the value of # EventType.HTTP_RESPONSE_START in this critical code path. 'type': 'http.response.start', 'status': resp_status, 'headers': resp._asgi_headers(default_media_type) }) await send(_EVT_RESP_EOF) # PERF(vytas): Check resp._registered_callbacks directly to shave # off a function call since this is a hot/critical code path. if resp._registered_callbacks: self._schedule_callbacks(resp) return # PERF(vytas): Operate directly on the resp private interface to reduce # overhead since this is a hot/critical code path. if resp._sse: sse_emitter = resp._sse if isasyncgenfunction(sse_emitter): raise TypeError( 'Response.sse must be an async iterable. This can be obtained by ' 'simply executing the async generator function and then setting ' 'the result to Response.sse, e.g.: resp.sse = some_asyncgen_function()' ) # NOTE(kgriffs): This must be done in a separate task because # receive() can block for some time (until the connection is # actually closed). async def watch_disconnect(): while True: received_event = await receive() if received_event['type'] == EventType.HTTP_DISCONNECT: break watcher = falcon.create_task(watch_disconnect()) await send({ 'type': EventType.HTTP_RESPONSE_START, 'status': resp_status, 'headers': resp._asgi_headers('text/event-stream') }) # PERF(vytas): Check resp._registered_callbacks directly to shave # off a function call since this is a hot/critical code path. if resp._registered_callbacks: self._schedule_callbacks(resp) handler, _, _ = self.resp_options.media_handlers._resolve( MEDIA_JSON, MEDIA_JSON, raise_not_found=False) # TODO(kgriffs): Do we need to do anything special to handle when # a connection is closed? async for event in sse_emitter: if not event: event = SSEvent() # NOTE(kgriffs): According to the ASGI spec, once the client # disconnects, send() acts as a no-op. We have to check # the connection state using watch_disconnect() above. await send({ 'type': EventType.HTTP_RESPONSE_BODY, 'body': event.serialize(handler), 'more_body': True }) if watcher.done(): break watcher.cancel() try: await watcher except asyncio.CancelledError: pass await send({'type': EventType.HTTP_RESPONSE_BODY}) return if data is not None: # PERF(kgriffs): Böse mußt sein. Operate directly on resp._headers # to reduce overhead since this is a hot/critical code path. # NOTE(kgriffs): We always set content-length to match the # body bytes length, even if content-length is already set. The # reason being that web servers and LBs behave unpredictably # when the header doesn't match the body (sometimes choosing to # drop the HTTP connection prematurely, for example). resp._headers['content-length'] = str(len(data)) await send({ # PERF(vytas): Inline the value of # EventType.HTTP_RESPONSE_START in this critical code path. 'type': 'http.response.start', 'status': resp_status, 'headers': resp._asgi_headers(default_media_type) }) await send({ # PERF(vytas): Inline the value of # EventType.HTTP_RESPONSE_BODY in this critical code path. 'type': 'http.response.body', 'body': data }) # PERF(vytas): Check resp._registered_callbacks directly to shave # off a function call since this is a hot/critical code path. if resp._registered_callbacks: self._schedule_callbacks(resp) return stream = resp.stream if not stream: resp._headers['content-length'] = '0' await send({ # PERF(vytas): Inline the value of # EventType.HTTP_RESPONSE_START in this critical code path. 'type': 'http.response.start', 'status': resp_status, 'headers': resp._asgi_headers(default_media_type) }) if stream: # Detect whether this is one of the following: # # (a) async file-like object (e.g., aiofiles) # (b) async generator # (c) async iterator # if hasattr(stream, 'read'): while True: data = await stream.read(self._STREAM_BLOCK_SIZE) if data == b'': break else: await send({ 'type': EventType.HTTP_RESPONSE_BODY, # NOTE(kgriffs): Handle the case in which data == None 'body': data or b'', 'more_body': True }) else: # NOTE(kgriffs): Works for both async generators and iterators try: async for data in stream: # NOTE(kgriffs): We can not rely on StopIteration # because of Pep 479 that is implemented starting # with Python 3.7. AFAICT this is only an issue # when using an async iterator instead of an async # generator. if data is None: break await send({ 'type': EventType.HTTP_RESPONSE_BODY, 'body': data, 'more_body': True }) except TypeError as ex: if isasyncgenfunction(stream): raise TypeError( 'The object assigned to Response.stream appears to ' 'be an async generator function. A generator ' 'object is expected instead. This can be obtained ' 'simply by calling the generator function, e.g.: ' 'resp.stream = some_asyncgen_function()') raise TypeError( 'Response.stream must be a generator or implement an ' '__aiter__ method. Error raised while iterating over ' 'Response.stream: ' + str(ex)) if hasattr(stream, 'close'): await stream.close() await send(_EVT_RESP_EOF) # PERF(vytas): Check resp._registered_callbacks directly to shave # off a function call since this is a hot/critical code path. if resp._registered_callbacks: self._schedule_callbacks(resp)
def validate_req_body(req, resp, resource, params): if "json" not in req.context: msg = "Empty response body. A valid JSON document is required." raise HTTPBadRequest("Bad request", msg)
def error_handler(ex, req, resp, params): raise HTTPBadRequest(type(ex).__name__, str(ex))