How can I support a POST request with XML as a body and XML as a response.
I was able to get that far:
mport typing
from fastapi import APIRouter
from simplexml import dumps
from starlette.responses import Response
router = APIRouter()
class XmlResponse(Response):
media_type = "text/xml"
def render(self, content: typing.Any) -> bytes:
return dumps({'response': content}).encode("utf-8")
@router.get("/items/scorer")
async def get_response():
"""An endpoint to return the global configuration
"""
return XmlResponse({'person':{'name':'joaquim','age':15,'cars':[{'id':1},{'id':2}]}})
And this returns an XML. However, the docs and redoc API are not returning anything.
I am seeing this in the logs:
```
[7697] [2019-09-22 13:37:06,432] [uvicorn] [DEBUG] [('127.0.0.1', 59158) - ASGI [4] Received {'type': 'http.response.start', 'status': 404, 'headers': '<...>'}]
[7697] [2019-09-22 13:37:06,432] [__main__] [INFO] [('127.0.0.1', 59158) - "GET /socs HTTP/1.1" 404]
````
Any help is very much appreciated!
GET /socs HTTP/1.1" 404
Looks like it might be a typo in your browser? Like, you tried to go to apihost.com/socs instead of apihost.com/docs.
Sorry about the typo. You are right about 404 error, however the page http://127.0.0.1:8080/docs comes black empty.
And I am not able to PST the XML in a body:
{
"detail": "There was an error parsing the body"
}
I am guessing I need to configure something to be able to accept the body in XML, just fail to find any documentations.
A sample on how to do it will be very much appreciated.
What do you mean it comes back empty? Are you getting a specific status code? What do the server logs say?
Currently, I don't think FastAPI supports XML as a body directly; you'd have to create a dependency that reads the object off from the raw XML content in the request body.
Can you share the code that you are using for your endpoint that accepts an XML body? If you can do that, it might be easy to draft a skeleton implementation that you might find helpful.
[16130] [2019-09-22 15:56:32,548] [__main__] [DEBUG] [('127.0.0.1', 61157) - Disconnected]
[16130] [2019-09-22 15:56:32,548] [__main__] [DEBUG] [('127.0.0.1', 61161) - Connected]
[16130] [2019-09-22 15:56:32,549] [__main__] [DEBUG] [('127.0.0.1', 61162) - Connected]
[16130] [2019-09-22 15:56:32,550] [uvicorn] [DEBUG] [('127.0.0.1', 61161) - ASGI [19] Started]
[16130] [2019-09-22 15:56:34,830] [uvicorn] [DEBUG] [('127.0.0.1', 61161) - ASGI [19] Received {'type': 'http.response.start', 'status': 200, 'headers': '<...>'}]
[16130] [2019-09-22 15:56:34,830] [__main__] [INFO] [('127.0.0.1', 61161) - "GET /docs HTTP/1.1" 200]
[16130] [2019-09-22 15:56:35,589] [uvicorn] [DEBUG] [('127.0.0.1', 61161) - ASGI [19] Received {'type': 'http.response.body', 'body': None}]
[16130] [2019-09-22 15:56:36,329] [uvicorn] [DEBUG] [('127.0.0.1', 61161) - ASGI [19] Completed]
[16130] [2019-09-22 15:56:40,592] [__main__] [DEBUG] [('127.0.0.1', 61161) - Disconnected]
And there is no response from the http://127.0.0.1:8080/docs call.
Here is my code:
import json
import typing
from fastapi import APIRouter, Header
from pydantic import BaseModel
from simplexml import dumps
from starlette.responses import Response
router = APIRouter()
class Item(BaseModel):
name: str
price: float
is_offer: bool = None
class XmlResponse(Response):
media_type = "text/xml"
def render(self, content: typing.Any) -> bytes:
return dumps({'response': content}).encode("utf-8")
@router.post("/items/scorer")
async def process_item(item: Item, header: str = Header(None)):
"""An endpoint to return the global configuration
"""
# TODO: read the item
return XmlResponse({'person':{'name':'joaquim','age':15,'cars':[{'id':1},{'id':2}]}})
Is this enough? Or would you like me to share the entire project?
From what I could tell it looks like I need to find a way to override/enhance: https://github.com/tiangolo/fastapi/blob/master/fastapi/routing.py , line 93 to be able to support both json and xml. Am I on the right path?
@agorina Yes that's right, but you might find yourself getting surprisingly deep into the fastapi/starlette internals. If you want to look into this more, you might take a look at this comment: https://github.com/tiangolo/fastapi/issues/521#issuecomment-532043464
In particular, fastapi is pretty hard-coded against json format for parsing pydantic models.
If you want to create a generic dependency that handles XML, the following might be a good start:
from typing import TypeVar, Generic, Type, Any
from fastapi import Depends, Header, APIRouter
from pydantic import BaseModel
from simplexml import dumps, loads
from starlette.requests import Request
from starlette.responses import Response
T = TypeVar("T", bound=BaseModel)
router = APIRouter()
class Item(BaseModel):
name: str
price: float
is_offer: bool = None
class XmlResponse(Response):
media_type = "text/xml"
def render(self, content: Any) -> bytes:
return dumps({'response': content}).encode("utf-8")
class XmlBody(Generic[T]):
def __init__(self, model_class: Type[T]):
self.model_class = model_class
async def __call__(self, request: Request) -> T:
# the following check is unnecessary if always using xml,
# but enables the use of json too
if request.headers.get("Content-Type") == "application/xml":
body = await request.body()
dict_data = loads(body)
else:
dict_data = await request.json()
return self.model_class.parse_obj(dict_data)
@router.post("/")
async def process_item(item: Item = Depends(XmlBody(Item)), header: str = Header(None)):
return XmlResponse({'person': {'name': 'joaquim', 'age': 15, 'cars': [{'id': 1}, {'id': 2}]}})
Note: this will not properly handle things like List[T]; the body must be parsed to a BaseModel (the typevar bound will ensure mypy complains if you don't do this).
It would be possible to remove this restriction in various ways, but the right way to do it may depend on your use case and/or the capabilities of simplexml (with which I am completely unfamiliar).
I'm afraid I'm not sure why the docs aren't working, but it looks like fastapi is saying it worked (since you got a 200 on the docs endpoint).
I'd recommend 1) ensuring you can view the docs if you use a simple example (e.g., from the tutorial); if this doesn't work, it is probably something with your local configuration; 2) if you can view the docs for a "simple" server, figure out what you've added that is causing the docs response to fail by including endpoints/etc. into the router/app one step at a time until you've isolated the problem source.
@dmontagu, thank you very much for a quick response!
With your sample code I was able to get the XML POST working. I had to do one correction and would like to confirm that it looks right to you:
instead of
if request.get("Content-Type") == "application/xml":
I had to do
if request.headers['content-type'] == "application/xml":
since the original version did not return anything. Please let me know if it does not look right.
As for the docs not working, I am afraid I have no explanation. After restarting my machine, it is now working....Let's consider it a user error on my part. Sorry for the false alarm.
Sorry that was a typo in my code -- should have been request.headers.get not request.get (I've now fixed it). (But yes, your fix is the correct idea.)
You may want to use request.headers.get instead of the direct __getitem__ (square brackets access) in order to prevent a KeyError if the header wasn't provided.
Thanks a lot for all the help @dmontagu!
@agorina it seems you were able to solve it with @demontagu's help, right?
May we close this issue now?
yes, thank you!
yes, thank you!
Hi @agorina would you happen to have a Git Gist/repo or anything to look for what you got to work with the XML & fast API? Thank you!
@bbartling , I am sorry but I do not have anything that I could share at this time.
Most helpful comment
@agorina Yes that's right, but you might find yourself getting surprisingly deep into the fastapi/starlette internals. If you want to look into this more, you might take a look at this comment: https://github.com/tiangolo/fastapi/issues/521#issuecomment-532043464
In particular, fastapi is pretty hard-coded against json format for parsing pydantic models.
If you want to create a generic dependency that handles XML, the following might be a good start:
Note: this will not properly handle things like
List[T]; the body must be parsed to aBaseModel(the typevar bound will ensure mypy complains if you don't do this).It would be possible to remove this restriction in various ways, but the right way to do it may depend on your use case and/or the capabilities of simplexml (with which I am completely unfamiliar).