0% found this document useful (0 votes)
22 views3 pages

Python With Fast API

The document addresses an OSError encountered when closing an SFTP connection prematurely in a FastAPI application, which leads to an invalid socket during file streaming. It provides a solution by suggesting to move the connection establishment inside the generator function, ensuring the connection remains open during the streaming process, and includes additional recommendations for error handling and security. The document also includes updated code for the `/download` endpoint and testing steps to verify functionality.

Uploaded by

tricka325
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
22 views3 pages

Python With Fast API

The document addresses an OSError encountered when closing an SFTP connection prematurely in a FastAPI application, which leads to an invalid socket during file streaming. It provides a solution by suggesting to move the connection establishment inside the generator function, ensuring the connection remains open during the streaming process, and includes additional recommendations for error handling and security. The document also includes updated code for the `/download` endpoint and testing steps to verify functionality.

Uploaded by

tricka325
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 3

The error you're encountering (`OSError: Socket is closed`) occurs because the SFTP connection

and transport are closed **before** the file streaming begins. In the `/download` endpoint:

- The `file_generator` function is a generator that opens and reads from the SFTP file lazily
(during response streaming).
- By closing `sftp` and `transport` immediately after defining the generator, the underlying socket
becomes invalid when the ASGI server (Uvicorn) later iterates over the generator in a threadpool
to send chunks.
- Paramiko's SFTP sessions are not designed to survive closure like this; the connection must
remain open for the entire duration of the file read/stream.

This is a common issue when mixing synchronous I/O libraries (like Paramiko) with async
FastAPI responses. Closing early invalidates the channel/socket.

### Solution
- **Move the connection inside the generator**: Establish the SSH/SFTP connection *inside*
`file_generator()`. This keeps it open during iteration. Clean up (close) in a `finally` block after
yielding all chunks.
- **Handle exceptions gracefully**: Wrap the generator logic to raise HTTP exceptions on SFTP
errors without crashing the ASGI app.
- **Use binary mode consistently**: Ensure `'rb'` and `'wb'` for ZIP files (binary data).
- **Additional fixes**:
- Use `io.BytesIO` or direct chunk yielding for efficiency.
- Add a small delay or keep-alive if network timeouts are suspected (but primary issue is
premature close).
- For large files, this streams efficiently without full memory load.
- In production, switch to an async SFTP library like `asyncssh` to avoid threadpool overhead
entirely (Paramiko is sync and blocks).

### Updated `/download` Endpoint Code


Replace your existing `@app.get("/download")` with this:

```python
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import StreamingResponse
import paramiko
import os
from typing import Generator

# ... (rest of your code, including SERVER_IP, etc.)

@app.get("/download")
async def download_zip(filename: str = Query(..., description="Name of the ZIP file to download
from server")):
if not filename.endswith(".zip"):
raise HTTPException(status_code=400, detail="Only ZIP files are supported")

remote_path = os.path.join(REMOTE_DOWNLOAD_DIR, filename).replace("\\", "/")


def file_generator() -> Generator[bytes, None, None]:
transport = None
sftp = None
remote_file = None
try:
# Establish connection INSIDE the generator (keeps socket alive during streaming)
transport = paramiko.Transport((SERVER_IP, 22))
transport.connect(username=USERNAME, password=PASSWORD)
sftp = paramiko.SFTPClient.from_transport(transport)

# Check existence (optional, but good for early fail)


try:
file_attrs = sftp.stat(remote_path)
if file_attrs.st_size == 0:
raise HTTPException(status_code=404, detail="File is empty")
except IOError:
raise HTTPException(status_code=404, detail=f"File '{filename}' not found on server")

# Open and stream in chunks


remote_file = sftp.open(remote_path, 'rb')
while True:
chunk = remote_file.read(1024 * 1024) # 1MB chunks; adjust as needed
if not chunk:
break
yield chunk

except Exception as e:
# Re-raise as HTTPException for FastAPI to handle cleanly
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail=f"Stream failed: {str(e)}")
finally:
# Clean up connections even if error occurs during streaming
try:
if remote_file:
remote_file.close()
except:
pass
try:
if sftp:
sftp.close()
except:
pass
try:
if transport:
transport.close()
except:
pass
return StreamingResponse(file_generator(), media_type="application/zip", headers={"Content-
Disposition": f"attachment; filename={filename}"})
```

### How This Fixes It


- **Connection Lifetime**: The transport/sftp now lives until the generator finishes yielding (i.e.,
entire file streamed). Closure happens in `finally` after the last chunk.
- **Error Propagation**: Exceptions inside the generator bubble up as HTTP 500/404, avoiding
raw socket errors in logs.
- **Streaming Safety**: Uses a generator with `yield`, compatible with `StreamingResponse`.
Runs in a threadpool via AnyIO (as before), but now socket stays open.
- **Resource Cleanup**: `finally` ensures no leaks even on client disconnect or errors.

### Testing Steps


1. Restart Uvicorn: `uvicorn main:app --reload` (replace `main` with your filename).
2. Trigger download:
- Browser: `http://localhost:8000/download?filename=yourfile.zip` (downloads directly; save to
`E:/dip/name/` manually).
- Curl: `curl -o E:/dip/name/yourfile.zip http://localhost:8000/download?filename=yourfile.zip`
3. Check logs: If file exists on server, it should stream without socket closure error.
4. If timeout persists (e.g., slow network):
- Increase chunk size or add `transport.set_keepalive(30)` after connect.
- Verify server connectivity: `ping 192.168.1.6` or manual SFTP test with `sftp
[email protected]`.

### Additional Recommendations


- **Security**: Hardcoded creds are risky – move to env vars: `import os; PASSWORD =
os.getenv("SSH_PASS")`. Use SSH keys for paramiko
(`pkey=paramiko.RSAKey.from_private_key_file(...)`).
- **Async Alternative**: For better performance, install `asyncssh` and rewrite with async
generators (no threadpool blocking).
- **File Listing**: If needed, add a `/list` endpoint to list files in `REMOTE_DOWNLOAD_DIR` via
`sftp.listdir()`.
- **Production**: Add auth (e.g., FastAPI dependencies with API keys), rate limiting, and logging
(e.g., `logging` module).

If this doesn't resolve or you get a new error (e.g., auth fail), share the updated traceback or
server logs!

You might also like