Requests Library experiencing RecursionError

What I’m trying to do:
I’m trying to use the Python Requests module to create a flexible API call function, but I’m having trouble getting it to operate properly. The code works as expected when run outside of the Anvil platform, but it has problems when run inside Anvil. The following error occurs specifically:

RecursionError: maximum recursion depth exceeded while calling a Python object
at /usr/local/lib/python3.7/ssl.py:518

Here is the code:

def _call_api(url, details={}, method="GET"):
    """
    Internal function to make HTTP requests using the requests library.

    Args:
        url (str): The URL to make the request to.
        details (dict, optional): Additional details to pass to the request, such as headers. Defaults to {}.
        method (str, optional): The HTTP method to use. Defaults to "GET".
        json (bool, optional): Whether to treat the response as JSON. Defaults to True.

    Returns:
        dict: A dictionary containing the status code, headers, and the response body.
    """
    try:
        method = method.upper() if isinstance(method, str) else "GET"
        if method not in ["GET", "POST", "PUT", "DELETE"]:
            raise ValueError("Invalid HTTP method specified")
        response = requests.request(method, url, **details)

        response.raise_for_status()
        status = response.status_code
        headers = response.headers
        body = response.json()["body"]
        result = {
            "status": status,
            "headers": headers,
            "body": body,
        }
    except requests.exceptions.HTTPError as e:
        result = {
            "status": e.response.status_code,
            "headers": e.response.headers,
            "body": e.response.json(),
        }
    except ValueError as e:
        result = {
            "status": 400,
            "headers": {},
            "body": {"error": str(e)},
        }

    return result

Note: I currently have the Professional Plan

What is the line that crashes?

I don’t see anything wrong with the way the request is managed, and I don’t understand what may cause the specific error you are mentioning, but I see that you are using a mutable as a default value for the details argument, and this can have nasty side effects.

The function does not modify the dictionary nor it passes it to another function, so it shouldn’t be a problem here, but the right thing to do would be something like this:

def _call_api(url, details=None, method="GET"):
    if details is None:
        details = {}

I’ve updated the function to include a condition to confirm the details parameter is not the incorrect type and am still having the same issue. The function errors on the line that the request is being called.

response = requests.request(method, url, **details)

This might be related: python - "RecursionError: maximum recursion depth exceeded" from ssl.py: `super(SSLContext, SSLContext).options.__set__(self, value)` - Stack Overflow

That would explain why it works for you locally and does not in Anvil, since the versions of the SSL library may be different.

The problem (the one I’m talking about, not the one you are asking about, sorry for the XY intrusion) is not that the type could be wrong, it’s that a function argument should never be assigned a mutable as default value.

See this: python mutable in arguments - Google Search

I did the same as @jshaffstall above and googled it, I also found someone complaining about the maximum length of the **details being set on the server side with ssl.py, you might want to check how many items are being passed to details with print(len(details)) etc. etc

Thanks, I tried implementing this solution, based on the Stack Overflow post you provided:

# this is the import solution indicated
import gevent.monkey
gevent.monkey.patch_all()

The is the error I get using that solution:

RuntimeError: cannot release un-acquired lock
at <frozen importlib._bootstrap>:107
called from <frozen importlib._bootstrap>:152
called from <frozen importlib._bootstrap>:983
called from <frozen importlib._bootstrap>:1006
called from /usr/local/lib/python3.7/importlib/__init__.py:127
called from /downlink/anvil_downlink_worker/__init__.py:184
called from ColorPaletteCreater, line 17

In the example that I am using, there “details” variable was an empty dict.

If I remove the “details” variable completely from the request, I am still getting the same error.

I just asked chatGTP for help and it suggested that the reason why it works on your machine and not on another is due to invalid SSL certificates, and to disable them.

This sounds like a horrible idea, but it might be on to something about the reason for the difference in behavior from the same code in two different places. (security settings being different)

Yeah, that’s sounds like a horrible idea lol.

The order of imports of requests and gevent.monkey seems to be important. Clicking through to the original Github issue from that Stack Overflow answer might give some ideas.

Ugg… I can’t seem to find any solution here. I have used the request library without any issues in the past. This is very head scratching.

Here’s an idea, what if you switch your server module to use the 3.10 python beta, maybe ssl.py will be different on that.

Exactly!

A function argument should never be a dict, a list or any mutable.

The problem you are reporting has nothing to do with the empty dict being used as default value for a function argument. I mentioned it in my first answer, and Jay already found the cause of that problem, which has nothing to do with that empty dict.

Before asking if I have a problem with a function, I try to remove any potential source of problems, and having details={} in the arguments of a function is a big potential cause of the most unexpected and seemingly unrelated problems.

I’ve updated it to resolve this issue. Here is the updated version:

def _call_api(url, details=None, method="GET"):

    details = details if isinstance(details, dict) else {}

Thanks for the advice!

Have you tried using the anvil http library?

import anvil.http
def _call_api(url, details=None, method="GET"):
    """
    Internal function to make HTTP requests using the requests library.

    Args:
        url (str): The URL to make the request to.
        details (dict, optional): Additional details to pass to the request, such as headers. Defaults to {}.
        method (str, optional): The HTTP method to use. Defaults to "GET".
        json (bool, optional): Whether to treat the response as JSON. Defaults to True.

    Returns:
        dict: A dictionary containing the status code, headers, and the response body.
    """
    details = details if isinstance(details, dict) else {}
    try:
        method = method.upper() if isinstance(method, str) else "GET"
        if method not in ["GET", "POST", "PUT", "DELETE"]:
            raise ValueError("Invalid HTTP method specified")

        response = anvil.http.request(method=method,url=url,**details,json=True)
        #response = requests.request(method, url, **details)

        #response.raise_for_status()
        #status = response.status
        #headers = response.headers
        body = response
        result = {
            "status": 200,
            "headers": None,
            "body": body,
        }
    except anvil.http.HttpError as e:
        result = {
            "status": e.status,
            "header": {},
            "body": e.content,
        }
    except ValueError as e:
        result = {
            "status": 400,
            "headers": {},
            "body": {"error": str(e)},
        }

    return result

EDIT had to change successful response because I am not getting the header or status
EDIT 2 Changed anvil.http.HTTPError to anvil.http.HttpError

Yeah, I had tried that too. I ended up getting this error when I switch to the 3.10 (beta):

anvil.server.RuntimeUnavailableError: Internal server error: Server runtime failed to start.
at ColorPaletteCreater, line 19
# this is line 19 that is throwing the error
request = anvil.server.call("utils_call_api_no_auth", "my-url-here")

Anthony, where are you getting the “status” from in the “result” dict?

Sorry I just saw that mistake and made the edit :sweat_smile:

This is definitely untested. I am not sure how to get the status/header on a successful response (The documents and api docs aren’t clear on that).

I am ignoring that portion just to test if it works.

EDIT

this is from the docs:

If the HTTP status code is successful (200-299), the anvil.http.request function returns the response body of the request. By default, this will be a Media object.