2

I have a FastAPI application for testing/development purposes. What I want is that any request that arrives to my app to automatically be sent, as is, to another app on another server, with exactly the same parameters and same endpoint. This is not a redirect, because I still want the app to process the request and return values as usual. I just want to initiate a similar request to a different version of the app on a different server, without waiting for the answer from the other server, so that the other app gets the request as if the original request was sent to it.

How can I achieve that? Below is a sample code that I use for handling the request:

@app.post("/my_endpoint/some_parameters")
def process_request(
    params: MyParamsClass,
    pwd: str = Depends(authenticate),
):
    # send the same request to http://my_other_url/my_endpoint/
    return_value = process_the_request(params)
    return return_value.as_json()
Chris
  • 18,724
  • 6
  • 46
  • 80
amit
  • 3,332
  • 6
  • 24
  • 32
  • @AndrewRyan I'd like to connect via HTTP. do you mean something like this? changing the header of my function to: def process_request(params:MyParamsClass,pwd=,request:Request) and then within my function: requests.request("POST",my_url,request) I'm not sure what the syntax should be – amit Nov 24 '22 at 03:15
  • @AndrewRyan my apologies if this seems too basic. I know how to send a request, I'm not sure how to get the original request json and headers given that my function gets a param class which assumes some structure – amit Nov 24 '22 at 04:12

1 Answers1

5

You could use the AsyncClient() from the httpx library, as described in this answer, as well as this answer and this answer (have a look at those answers for more details on the approach demonstrated below). You can spawn a Client inside the startup event handler, store it on the app instance—as described here, as well as here and here—and reuse it every time you need it. You can explicitly close the Client once you are done with it, using the shutdown event handler.

Working Example

The Main Server

When building the request that is about to be forwarded to the other server, the main server uses request.stream() to read the request body from the client's request, which provides an async iterator, so that if the client sent a request with some large body (for instance, the client uploads a large file), the main server would not have to wait for the entire body to be received and loaded into memory before forwarding the request, something that would happen in case you used await request.body() instead, which would likely cause server issues if the body could not fit into RAM.

You can add multiple routes in the same way the /upload one has been defined below, specifying the path, as well as the HTTP method for the endpoint. Note that the /upload route below uses Starlette's path convertor to capture arbitrary paths, as demonstrated here and here. You could also specify the exact path parameters if you wish, but the below provides a more convenient way if there are too many of them. Regardless, the path will be evaluated against the endpoint in the other server below, where you can explicitly specify the path parameters.

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from starlette.background import BackgroundTask
import httpx

app = FastAPI()

@app.on_event('startup')
async def startup_event():
    client = httpx.AsyncClient(base_url='http://127.0.0.1:8001/')  # this is the other server
    app.state.client = client


@app.on_event('shutdown')
async def shutdown_event():
    client = app.state.client
    await client.aclose()


async def _reverse_proxy(request: Request):
    client = request.app.state.client
    url = httpx.URL(path=request.url.path, query=request.url.query.encode('utf-8'))
    req = client.build_request(
        request.method, url, headers=request.headers.raw, content=request.stream()
    )
    r = await client.send(req, stream=True)
    return StreamingResponse(
        r.aiter_raw(),
        status_code=r.status_code,
        headers=r.headers,
        background=BackgroundTask(r.aclose)
    )


app.add_route('/upload/{path:path}', _reverse_proxy, ['POST'])


if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8000)

The Other Server

Again, for simplicity, the Request object is used to read the body, but you can isntead define UploadFile, Form and other parameters as usual. In the example below, the server is listenning on port 8001.

from fastapi import FastAPI, Request

app = FastAPI()

@app.post('/upload/{p1}/{p2}')
async def upload(p1: str, p2: str, q1: str, request: Request):
    return {'p1': p1, 'p2': p2, 'q1': q1, 'body': await request.body()}
    
    
if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8001)

Test the above example using httpx

import httpx

url = 'http://127.0.0.1:8000/upload/hello/world'
files = {'file': open('file.txt', 'rb')}
params = {'q1': 'This is a query param'}
r = httpx.post(url, params=params, files=files)
print(r.content)
Chris
  • 18,724
  • 6
  • 46
  • 80