pythonazure-functionshttp-error

Error message in Azure Function Logs: 'Unexpected end of request content', status_code=0


I have an Azure Function that receives an HTTP Request containing JSON data. The function is supposed to filter the incoming JSON type and, upon identifying the specified JSON, extract certain values from it, creating a new JSON, and sending it in a POST request to another Azure Function. However, the error 'Unexpected end of request content' sometimes appears in the Azure Logs. I've added several Try-Except blocks to try to capture this error, but so far, no success.

@app.route(route="webhooks/baas", auth_level=func.AuthLevel.ANONYMOUS)
def webhook_decoder_baas(req: func.HttpRequest) -> func.HttpResponse:
    
    # Imports and decodes the private_key used for sending extracted information
    private_key = os.environ["private_key_baas"]
    private_key_baas = base64.b64decode(private_key).decode("utf-8")
    
    # Declares the public_key for decoding the received webhook
    qi_public_key = '''-----BEGIN PUBLIC KEY-----
... :)
-----END PUBLIC KEY-----'''

    # Checks if the received webhook contains the correct parameters
    try:
        authorization = req.headers.get("AUTHORIZATION")
        body = req.get_json()

        # If AUTHORIZATION header is missing, returns an error.
        if not authorization:
            logging.error("AUTHORIZATION header not provided.")
            return HttpResponse("AUTHORIZATION header not provided.")
        
        # If no body content was sent, returns an error.
        if not body:
            logging.error("BODY content not provided.")
            return HttpResponse("BODY content not provided.")
        
        # Decodes the request using the PUBLIC_KEY.
        decoded_header = jwt.decode(token=authorization, key=qi_public_key)
        
        # Validates received data, returns error if any doesn't match.
        try:
            assert decoded_header.get("method") == "POST"
        except AssertionError as assertion_error:
            logging.error(f"Error in the sending method, a different method than POST was used.")
            return HttpResponse(f"Error in the sending method, a different method than POST was used.")
        
        try:
            assert decoded_header.get("payload_md5") == md5(json.dumps(body).encode()).hexdigest()
        except AssertionError as assertion_error:
            logging.error(f"Payload hash does not match the expected one.")
            return HttpResponse(f"Payload hash does not match the expected one.")
        
        try:
            assert (datetime.utcnow() - timedelta(minutes=5)) < datetime.strptime(decoded_header.get("timestamp"), "%Y-%m-%dT%H:%M:%S.%fZ") < (datetime.utcnow() + timedelta(minutes=5))
        except AssertionError as assertion_error:
            logging.error(f"Error in sending timestamp, outside the 10-minute limit.")
            return HttpResponse(f"Error in sending timestamp, outside the 10-minute limit.")
                
    # Handles standard errors from the JWT library for errors in the received webhook token
    except jwt.JWTError as jwt_error:
        logging.error(f"Token error: {str(jwt_error)}")
        return HttpResponse(f"Token error: {str(jwt_error)}")
    
    # Handles standard Python errors
    except Exception as e:
        logging.error(f"Internal error while processing the webhook: {str(e)}")
        return HttpResponse(f"Internal error while processing the webhook: {str(e)}")

    # Converts received data into a PYTHON object
    payload_string = json.dumps(body)
    objeto_payload = json.loads(payload_string)
        
    # Verifies if the received data meets the desired requirements
    if (
        objeto_payload.get("webhook_type") == "bank_slip.status_change" and 
        (objeto_payload.get("status") == "payment_notice" or objeto_payload.get("status") == "notary_office_payment_notice")
    ):
            
        # Extracts desired data from the received data
        bank_slip_key = objeto_payload.get("data", {}).get("bank_slip_key")
        paid_amount = objeto_payload.get("data", {}).get("paid_amount")

        # Creates a new PYTHON object with the extracted data
        payload_output = {
            key: objeto_payload[key] for key in ["status"]
        }

        payload_output['Paid amount'] = paid_amount
        payload_output['Query key of the title'] = bank_slip_key

        # URL to send the extracted data
        url = "https://nerowebhooks.azurewebsites.net/api/information/send"

        # Encrypts the data to be sent
        token = jwt.encode(payload_output, private_key_baas, algorithm='ES512')

        # Creates the sending header
        headers = {
            'SIGNATURE': token
        }

        # Sends the extracted information
        response = requests.post(url, headers=headers, json=payload_output)

        logging.info(f"Received webhook: {payload_output}")
        return HttpResponse("Webhook received successfully!", status_code=200)
    
    else:
        logging.info("Webhook received successfully, but it won't be handled at the moment!")
        return HttpResponse("Webhook received successfully, but it won't be handled at the moment!")

Here is the type of JSON that I expect to receive:

{ "key": "03c38d18-d12f-4b5f-841c-afab52fe33c5", "data": { "our_number": 142, "paid_amount": 6676.38, "payment_bank": 104, "bank_slip_key": "03c38d18-d12f-4b5f-841c-afab52fe33c5", "payment_method": 2, "payment_origin": 3, "paid_in": { "name": "QI TECH", "code_number": "329", "ispb": "32402502" }, "occurrence_type": "payment_notice", "occurrence_feedback": "confirmed", "occurrence_sequence": 0, "requester_profile_code": "329-09-0001-0082162", "registration_institution": "qi_scd", "cnab_file_occurrence_order": 1, "registration_institution_occurrence_date": "2021-04-19" }, "status": "payment_notice", "webhook_type": "bank_slip.status_change", "event_datetime": "2021-04-19 20:04:06" }

But I can also receive various types of JSON, some larger, some smaller. During the testing phase, I sent all kinds of JSON, and when it should capture errors, the code did so effectively.


Solution

  • Instead of directly using req.get_json(), try using req.get_body() and then parsing it as JSON. This way, you can explicitly handle any exceptions that might occur during parsing.

    try:
        body = req.get_json()
    except ValueError as json_error:
        logging.error(f"Error parsing JSON: {str(json_error)}")
        return func.HttpResponse("Error parsing JSON", status_code=400)
    
    import os
    import base64
    import json
    import jwt
    from hashlib import md5
    from datetime import datetime, timedelta
    import azure.functions as func
    import logging
    import requests
    from http.client import IncompleteRead
    
    def webhook_decoder_baas(req: func.HttpRequest) -> func.HttpResponse:
        try:
            private_key = os.environ["private_key_baas"]
            private_key_baas = base64.b64decode(private_key).decode("utf-8")
            qi_public_key = '''-----BEGIN PUBLIC KEY-----
            ... :)
            -----END PUBLIC KEY-----'''
    
            authorization = req.headers.get("AUTHORIZATION")
    
            if not authorization:
                logging.error("AUTHORIZATION header not provided.")
                return func.HttpResponse("AUTHORIZATION header not provided.", status_code=400)
    
            try:
                body = req.get_body()
                if body is None or len(body) == 0:
                    logging.error("Empty or missing request body.")
                    return func.HttpResponse("Empty or missing request body.", status_code=400)
                
                body_json = json.loads(body)
            except ValueError as json_error:
                logging.error(f"Error parsing JSON: {str(json_error)}")
                return func.HttpResponse("Error parsing JSON", status_code=400)
    
            except IncompleteRead as incomplete_read_error:
                logging.error(f"Incomplete read error: {str(incomplete_read_error)}")
                return func.HttpResponse("Incomplete read error", status_code=400)
    
            decoded_header = jwt.decode(token=authorization, key=qi_public_key)
    
            assert decoded_header.get("method") == "POST", "Error in the sending method, a different method than POST was used."
            assert decoded_header.get("payload_md5") == md5(json.dumps(body_json).encode()).hexdigest(), "Payload hash does not match the expected one."
            assert (datetime.utcnow() - timedelta(minutes=5)) < datetime.strptime(decoded_header.get("timestamp"), "%Y-%m-%dT%H:%M:%S.%fZ") < (datetime.utcnow() + timedelta(minutes=5)), "Error in sending timestamp, outside the 10-minute limit."
    
            # Your existing JSON processing logic...
            
            # Your data extraction and sending logic...
    
            logging.info(f"Received webhook: {payload_output}")
            return func.HttpResponse("Webhook received successfully!", status_code=200)
    
        except jwt.JWTError as jwt_error:
            logging.error(f"Token error: {str(jwt_error)}")
            return func.HttpResponse(f"Token error: {str(jwt_error)}", status_code=400)
    
        except AssertionError as assertion_error:
            logging.error(f"Assertion error: {str(assertion_error)}")
            return func.HttpResponse(str(assertion_error), status_code=400)
    
        except Exception as e:
            logging.error(f"Internal error while processing the webhook: {str(e)}")
            return func.HttpResponse(f"Internal error while processing the webhook: {str(e)}", status_code=500)
    

    It will catch the IncompleteRead exception that might occur while reading the request content and handle it gracefully by returning a 400 Bad Request response.

    Result:

    2023-12-28T12:00:00.000 [Information] Executing 'webhook_decoder_baas' (Reason='This function was programmatically called via an HTTP request.')
    2023-12-28T12:00:01.000 [Error] AUTHORIZATION header not provided.
    2023-12-28T12:00:02.000 [Information] Executed 'webhook_decoder_baas' (Succeeded, Duration=1000ms)  
    

    Reference: