Skip to content

Memory Error when sending a get_request AFTER using http server with AP on CP9 beta 2 #8968

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

Closed
louisprp opened this issue Feb 22, 2024 · 10 comments · Fixed by #9867
Closed

Comments

@louisprp
Copy link

CircuitPython version

Adafruit CircuitPython 9.0.0-beta.2 on 2024-02-20; Waveshare ESP32-S2-Pico with ESP32S2

Code/REPL

### Starting the Server

mdns_server = mdns.Server(wifi.radio)
mdns_server.hostname = MDNS_HOSTNAME
mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=80)

pool = socketpool.SocketPool(wifi.radio)
server = HTTPServer(pool, "/static", debug=True)

@server.route("/")
def base(request: Request):
    return Response(request, "Hello from the CircuitPython HTTP Server!")

server.start(str(wifi.radio.ipv4_address_ap), 80)

### Stopping the Server

server.stop()
mdns_server.deinit()
wifi.radio.stop_ap()

### Then using requests (connected to Wi-Fi with internet connection)

try:
    url = "https://www.adafruit.com/api/quotes.php"
    #  pings adafruit quotes
    print("Fetching text from %s" % url)
    #  gets the quote from adafruit quotes
    response = requests.get(url)
    print("-" * 40)
    #  prints the response to the REPL
    print("Headers: ", response.headers)
    print("-" * 40)
    response.close()
except Exception as e:
    print("Error:\n", str(e))

Behavior

Traceback (most recent call last):
  File "requests.py", line 139, in get_info
  File "adafruit_requests.py", line 752, in get
  File "adafruit_requests.py", line 691, in request
  File "adafruit_requests.py", line 510, in _get_socket
MemoryError: 

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "code.py", line 132, in <module>
  File "requests.py", line 148, in get_info

Description

It seems that there is a MemoryError when doing a get request after using the socket (?) for an http server. I'm utilizing the server along with the AP. When Im not starting the server beforehand, the requests work fine. Not sure what the problem is here. Under previous version 8.x this seems to have worked fine on the ESP32-S2.

Additional information

No response

@louisprp louisprp added the bug label Feb 22, 2024
@louisprp louisprp changed the title Memory Error when sending a get_request AFTER using http server with AP Memory Error when sending a get_request AFTER using http server with AP on CP9 beta 2 Feb 22, 2024
@tannewt tannewt modified the milestones: Long term, 9.0.0 Feb 22, 2024
@tannewt
Copy link
Member

tannewt commented Mar 5, 2024

Please post the complete example. This one has no imports. I wasn't able to reproduce it because they are missing.

@tannewt tannewt modified the milestones: 9.0.0, Long term Mar 6, 2024
@dhalbert
Copy link
Collaborator

Could you retry with the latest ConnectionManager library? Thanks. adafruit/Adafruit_CircuitPython_ConnectionManager#16 has fixed several other issues that seem similar.

@chadj
Copy link

chadj commented Dec 3, 2024

I've run into this problem on a similar board (unexpected maker FeatherS2)

CircuitPython version

Adafruit CircuitPython 9.2.1 on 2024-11-20; FeatherS2 with ESP32S2
Board ID:unexpectedmaker_feathers2
UID:487F3027BD2B
boot.py output:

Here's a complete code snippet that reproduces the error. It is based on the code above. I'm using the latest versions of adafruit_connection_manager, adafruit_requests and adafruit_httpserver. Also worth mentioning. I have the CircuitPython Web Workflow Code Editor enabled. So the code below isn't initializing wifi. To get the error to happen hit the webserver running on the esp32 at "/".

import wifi
import adafruit_connection_manager
import adafruit_requests
from adafruit_httpserver import Server, Request, Response, GET

# print(f"Connecting to {os.getenv("CIRCUITPY_WIFI_SSID")}...")
# wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
# print(f"Connected to {os.getenv("CIRCUITPY_WIFI_SSID")}")

pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)

### Starting the Server

server = Server(pool, debug=True)

@server.route("/")
def base(request: Request):
    url = "https://www.adafruit.com/api/quotes.php"
    #  pings adafruit quotes
    print("Fetching text from %s" % url)
    #  gets the quote from adafruit quotes
    response = requests.get(url)
    print("-" * 40)
    #  prints the response to the REPL
    print("Headers: ", response.headers)
    print("-" * 40)
    response.close()

    return Response(request, "Hello from the CircuitPython HTTP Server!")

server.serve_forever(str(wifi.radio.ipv4_address))

Here's the error:

Traceback (most recent call last):
  File "adafruit_httpserver/server.py", line 416, in poll
  File "adafruit_httpserver/server.py", line 346, in _handle_request
  File "adafruit_httpserver/server.py", line 330, in wrapped_handler
  File "code.py", line 25, in base
  File "adafruit_requests.py", line 711, in get
  File "adafruit_requests.py", line 639, in request
  File "adafruit_connection_manager.py", line 337, in get_socket
  File "adafruit_connection_manager.py", line 240, in _get_connected_socket
MemoryError: 

@bablokb
Copy link

bablokb commented Dec 4, 2024

The requests-module (to be specific: the Session class) only maintains a single request. In your request handler, you create a new request while processing the old one (this closes the old request). So this might be something that later trips the native method wrap_socket().

@anecdata
Copy link
Member

anecdata commented Dec 4, 2024

I tried to recreate the original post code, but with latest CircuitPython and libraries, and use of ConnectionManager (due especially to multiple uses of sockets). I'm not sure why the partial original gets a memory exception, but this version looks fine with regard to use of RAM:

Adafruit CircuitPython 9.2.1 on 2024-11-20; Adafruit Feather ESP32-S2 TFT with ESP32S2

code.py output:
# ...snip...
after stop gc.mem_free()=1978416
code.py...
import time
time.sleep(3)  # wait for serial

import gc
print(f"code start {gc.mem_free()=}")

import os
import wifi
import mdns
import socketpool
import adafruit_requests
import adafruit_connection_manager
from adafruit_httpserver import Server, Request, Response
print(f"after imports {gc.mem_free()=}")

# start wifi Station
wifi.radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD"))

# start wifi AP
wifi.radio.start_ap("test", "passw0rd")
print(f"after AP {gc.mem_free()=}")

# start mDNS
mdns_server = mdns.Server(wifi.radio)
mdns_server.hostname = "MDNS_HOSTNAME"
mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=80)
print(f"after mDNS {gc.mem_free()=}")

# init socket pool
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
print(f"{gc.mem_free()=}")

# init Requests
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)
print(f" after requests {gc.mem_free()=}")

# start HTTP Server
http_server = Server(pool, "/static", debug=True)
print(f"after HTTP Server init {gc.mem_free()=}")

@http_server.route("/")
def base(request: Request):
    return Response(request, "Hello from the CircuitPython HTTP Server!")

http_server.start(str(wifi.radio.ipv4_address_ap), 80)
print(f"after HTTP server start {gc.mem_free()=}")

# stop everything
http_server.stop()
mdns_server.deinit()
wifi.radio.stop_ap()
print(f"after stop {gc.mem_free()=}")

### Then using requests (connected to Wi-Fi with internet connection)

try:
    url = "https://www.adafruit.com/api/quotes.php"
    #  pings adafruit quotes
    print("Fetching text from %s" % url)
    #  gets the quote from adafruit quotes
    response = requests.get(url)
    print("-" * 40)
    #  prints the response to the REPL
    print("Headers: ", response.headers)
    print("-" * 40)
    response.close()
except Exception as e:
    print("Error:\n", str(e))
print(f"after stop {gc.mem_free()=}")

It's possible that gc.mem_free() isn't showing the portion of memory that is running out. Not everything runs from the heap.

@chadj
Copy link

chadj commented Dec 4, 2024

Re: @bablokb
Maybe I'm confused here. But there's the request object being passed into my method from adafruit_httpserver and the requests object coming from adafruit_requests that I am defining in my code. You're suggesting the two are conflicting somehow?

Re: @anecdata
I ran your code snippet and it failed with a memory error. Here's the output:

code start gc.mem_free()=7745728
after imports gc.mem_free()=7668720
after AP gc.mem_free()=7668544
after mDNS gc.mem_free()=7668464
gc.mem_free()=7668256
 after requests gc.mem_free()=7668016
after HTTP Server init gc.mem_free()=7666208
Started development server on http://192.168.4.1:5000
after HTTP server start gc.mem_free()=7664960
Stopped development server
after stop gc.mem_free()=7664864
Fetching text from https://www.adafruit.com/api/quotes.php
Error:
 MemoryError
after stop gc.mem_free()=7661776

I do wonder if this is somehow PSRAM related. The UM FeatherS2 has 8mb there. Maybe I'll try running this with the CircuitPython code editor disabled. That might save some ram.

@anecdata
Copy link
Member

anecdata commented Dec 4, 2024

@chadj I haven't reproduced that (yet?). I added some code in the middle to allow clients to connect (my phone, connected to the S2 AP, then connecting to the HTTP server by IP address) and that worked too in brief testing.

code.py...
import time
time.sleep(3)  # wait for serial

import gc
print(f"code start {gc.mem_free()=}")

import os
import wifi
import mdns
import socketpool
import adafruit_requests
import adafruit_connection_manager
from adafruit_httpserver import Server, Request, Response
print(f"after imports {gc.mem_free()=}")

# start wifi Station
wifi.radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD"))

# start wifi AP
wifi.radio.start_ap("test", "passw0rd")
print(f"after AP {gc.mem_free()=}")

# start mDNS
mdns_server = mdns.Server(wifi.radio)
mdns_server.hostname = "MDNS_HOSTNAME"
mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=80)
print(f"after mDNS {gc.mem_free()=}")

# init socket pool
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
print(f"{gc.mem_free()=}")

# init Requests
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)
print(f" after requests {gc.mem_free()=}")

# start HTTP Server
http_server = Server(pool, "/static", debug=True)
print(f"after HTTP Server init {gc.mem_free()=}")

@http_server.route("/")
def base(request: Request):
    return Response(request, "Hello from the CircuitPython HTTP Server!")

http_server.start(str(wifi.radio.ipv4_address_ap), 80)
print(f"after HTTP server start {gc.mem_free()=}")

print(f"waiting for connection from HTTP client...")
start = time.monotonic_ns()
while time.monotonic_ns() - start < 60_000_000_000:
    http_server.poll()

# stop everything
http_server.stop()
mdns_server.deinit()
wifi.radio.stop_ap()
print(f"after stop {gc.mem_free()=}")

### Then using requests (connected to Wi-Fi with internet connection)

try:
    url = "https://www.adafruit.com/api/quotes.php"
    #  pings adafruit quotes
    print("Fetching text from %s" % url)
    #  gets the quote from adafruit quotes
    response = requests.get(url)
    print("-" * 40)
    #  prints the response to the REPL
    print("Headers: ", response.headers)
    print("-" * 40)
    response.close()
except Exception as e:
    print("Error:\n", str(e))
print(f"after stop {gc.mem_free()=}")
full output...
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
code start gc.mem_free()=1945728
after imports gc.mem_free()=1999728
after AP gc.mem_free()=1999568
after mDNS gc.mem_free()=1999472
gc.mem_free()=1999264
 after requests gc.mem_free()=1999024
after HTTP Server init gc.mem_free()=1997216
Started development server on http://192.168.4.1:80
after HTTP server start gc.mem_free()=1995968
waiting for connection from HTTP client...
192.168.4.2 -- "GET /" 400 -- "200 OK" 125 -- 19ms
192.168.4.2 -- "GET /favicon.ico" 351 -- "404 Not Found" 131 -- 22ms
192.168.4.2 -- "GET /" 400 -- "200 OK" 125 -- 18ms
192.168.4.2 -- "GET /" 400 -- "200 OK" 125 -- 20ms
Stopped development server
after stop gc.mem_free()=1956752
Fetching text from https://www.adafruit.com/api/quotes.php
----------------------------------------
Headers:  {'expires': 'Thu, 19 Nov 1981 08:52:00 GMT', 'access-control-allow-methods': 'GET, POST, OPTIONS', 'strict-transport-security': 'max-age=15552000; preload', 'cf-ray': '8ece5c38fa55f603-ORD', 'access-control-max-age': '1728000', 'server': 'cloudflare', 'access-control-allow-credentials': 'true', 'x-frame-options': 'DENY', 'connection': 'keep-alive', 'transfer-encoding': 'chunked', 'content-type': 'application/json', 'cache-control': 'no-store, no-cache, must-revalidate', 'date': 'Wed, 04 Dec 2024 19:56:31 GMT', 'x-content-type-options': 'nosniff', 'cf-cache-status': 'DYNAMIC', 'access-control-allow-headers': 'Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Authorization, Referer, User-Agent', 'pragma': 'no-cache', 'set-cookie': 'zenid=dq15vrlnbvt062arntsk4ikqvf; expires=Wed, 18-Dec-2024 19:56:31 GMT; Max-Age=1209600; path=/; domain=.adafruit.com; secure; HttpOnly, cart_count=0; expires=Wed, 18-Dec-2024 19:56:31 GMT; Max-Age=1209600; path=/; domain=.adafruit.com, wishlist=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.adafruit.com'}
----------------------------------------
after stop gc.mem_free()=1939152

Code done running.

It would be very odd if more PSRAM triggered the issue.

P.S. Also seems OK if I access the HTTP Server using its mDNS name.

@jepler
Copy link

jepler commented Dec 4, 2024

239         if is_ssl:
240             socket = ssl_context.wrap_socket(socket, server_hostname=host)

fwiw the failing call is to ssl_context.wrap_socket.

We take the default esp-idf configuration and restrict allocation of mbedtls data to internal SRAM:

    choice MBEDTLS_MEM_ALLOC_MODE
        prompt "Memory allocation strategy"
        default MBEDTLS_INTERNAL_MEM_ALLOC 
        help
            Allocation strategy for mbedTLS, essentially provides ability to
            allocate all required dynamic allocations from,
…
            Recommended mode here is always internal (*), since that is most preferred
            from security perspective. But if application requirement does not
            allow sufficient free internal memory then alternate mode can be  
            selected.

It would be interesting to see if this failure went away with the setting MBEDTLS_EXTERNAL_MEM_ALLOC, with some reduction in security (i.e., if someone takes apart your powered device they can read out some key data from the PSRAM chip!). In CircuitPython we probably don't care about such a scenario.

@chadj
Copy link

chadj commented Dec 4, 2024

@jepler
I built the espressif-mbedtls-psram branch from your fork to test the proposed fix. I flashed it. I then looked at boot_out.txt to verify that the new version was running. And sure enough, my code now runs.

Surprise twist though. I flashed back to stock 9.2.1 and tried my code again. And surprise, my code worked! I verified boot_out.txt. I'm running stock 9.2.1 from 2024-11-20 and it executes now without throwing a MemoryError. So there might not be an issue to begin with.

When I initially ran into this issue and then did subsequent testing I never power cycled the device. I'm wondering if a power cycle cleared up the problem. I'm still trying to get the MemoryError to throw. I'll update this thread if I'm able to figure out how to duplicate the problem

@chadj
Copy link

chadj commented Dec 5, 2024

Ok
I now have a code snippet that reliably reproduces the MemoryError on the FeatherS2 from a fresh boot. It's based on code from @anecdata but with a few more imports (for audio handling).

On stock 9.2.1 it fails. With the patch from @jepler it works. So the change to allow mbedtls psram allocation solves this problem.

import time
time.sleep(5)  # wait for serial

import gc
print(f"code start {gc.mem_free()=}")

import os
import wifi
import mdns
import socketpool
import adafruit_requests
import adafruit_connection_manager
from adafruit_httpserver import Server, Request, Response

import board
import math
import audiocore
import audiobusio
import array

print(f"after imports {gc.mem_free()=}")

# start wifi Station
#wifi.radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD"))
wifi.radio.connect("<set ap>", "<set password>")

# start wifi AP
wifi.radio.start_ap("test", "passw0rd")
print(f"after AP {gc.mem_free()=}")

# start mDNS
mdns_server = mdns.Server(wifi.radio)
mdns_server.hostname = "MDNS_HOSTNAME"
mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=5000)
print(f"after mDNS {gc.mem_free()=}")

# init socket pool
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
print(f"{gc.mem_free()=}")

# init Requests
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)
print(f" after requests {gc.mem_free()=}")

# start HTTP Server
http_server = Server(pool, "/static", debug=True)
print(f"after HTTP Server init {gc.mem_free()=}")

@http_server.route("/")
def base(request: Request):
    return Response(request, "Hello from the CircuitPython HTTP Server!")

http_server.start(str(wifi.radio.ipv4_address_ap), 5000)
print(f"after HTTP server start {gc.mem_free()=}")

sample_rate = 8000
tone_volume = .1  # Increase or decrease this to adjust the volume of the tone.
frequency = 440  # Set this to the Hz of the tone you want to generate.
length = sample_rate // frequency  # One freqency period
sine_wave = array.array("H", [0] * length)
for i in range(length):
    sine_wave[i] = int((math.sin(math.pi * 2 * frequency * i / sample_rate) *
                        tone_volume + 1) * (2 ** 15 - 1))

audio = audiobusio.I2SOut(board.D1, board.D0, board.D9)
sine_wave_sample = audiocore.RawSample(sine_wave, sample_rate=sample_rate)
audio.play(sine_wave_sample, loop=False)
print(f"after audio init {gc.mem_free()=}")

# stop everything
http_server.stop()
mdns_server.deinit()
wifi.radio.stop_ap()
print(f"after stop {gc.mem_free()=}")

### Then using requests (connected to Wi-Fi with internet connection)

try:
    url = "https://www.adafruit.com/api/quotes.php"
    #  pings adafruit quotes
    print("Fetching text from %s" % url)
    #  gets the quote from adafruit quotes
    response = requests.get(url)
    print("-" * 40)
    #  prints the response to the REPL
    print("Headers: ", response.headers)
    print("-" * 40)
    response.close()
except Exception as e:
    print("Error:\n", str(e), e.__class__.__name__)
print(f"after stop {gc.mem_free()=}")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants