Fastapi: HHTP request/response with application-content = text/xml

Created on 22 Sep 2019  路  12Comments  路  Source: tiangolo/fastapi

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!

question

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:

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).

All 12 comments

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.

  1. What do you mean it comes back empty? Are you getting a specific status code? What do the server logs say?

  2. 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.

  1. The logs include this (does not look helpful to me):
[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.

  1. _"you'd have to create a dependency that reads the object off from the raw XML content in the request body."_ - could you please provide an example? Maybe point me to how it is done for the JSON content so that I can follow the pattern?

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.

Was this page helpful?
0 / 5 - 0 ratings