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 `[Link]` 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 `@[Link]("/download")` with this:
```python
from fastapi import FastAPI, HTTPException, Query
from [Link] import StreamingResponse
import paramiko
import os
from typing import Generator
# ... (rest of your code, including SERVER_IP, etc.)
@[Link]("/download")
async def download_zip(filename: str = Query(..., description="Name of the ZIP file to download
from server")):
if not [Link](".zip"):
raise HTTPException(status_code=400, detail="Only ZIP files are supported")
remote_path = [Link](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 = [Link]((SERVER_IP, 22))
[Link](username=USERNAME, password=PASSWORD)
sftp = [Link].from_transport(transport)
# Check existence (optional, but good for early fail)
try:
file_attrs = [Link](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 = [Link](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:
[Link]()
except:
pass
try:
if transport:
[Link]()
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: `[Link] (downloads directly; save to
`E:/dip/name/` manually).
- Curl: `curl -o E:/dip/name/[Link] [Link]
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 [Link]` or manual SFTP test with `sftp
oracle@[Link]`.
### Additional Recommendations
- **Security**: Hardcoded creds are risky – move to env vars: `import os; PASSWORD =
[Link]("SSH_PASS")`. Use SSH keys for paramiko
(`pkey=[Link].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
`[Link]()`.
- **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!