Search code examples
swaggerhttp-status-code-404openapi

How do I create the OpenAPI section for the 404 page?


I'm using OpenApi 3. A tool I use, Owasp Zap looks at the OpenAPI doc and creates fake requests. When it gets a 404, it complains that it doesn't have the media type that the OpenAPI promises.

But I didn't write anything in the OpenAPI doc about how 404s are handled. Obviously I can't write an infinite number of bad end points & document that they return 404s.

What is the right way to record this in the OpenAPI yaml or json?

Here is a minimal yaml file... I know for sure that this file does say anything about 404, ie. 404s aren't in the contract so tools are complaining that 404s are valid responses, but 404 is what a site should return when a resource is missing

---
"openapi": "3.0.0"

paths:
    /Foo/:
        get:
            responses:
                "200":
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/Foo"
                default:
                    description: Errors
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/Error"
components:
    schemas:
        Foo:
            type: object
            required:
                - name
            properties:
                name:
                    type: string
        Error:
            type: object
            required:
                - error
            properties:
                error:
                    type: string
                message:
                    type: string
                data:
                    type: object

Solution

  • This has been proposed already but not implemented: https://github.com/OAI/OpenAPI-Specification/issues/521

    In the comments someone gave a suggestion: https://github.com/OAI/OpenAPI-Specification/issues/521#issuecomment-513055351, which reduces a little your code, but you would still have to insert N*M entries for N paths * M methods.

    Since we don't have the ability to make the specification change to our needs, all that remains is we adapting ourselves.

    From your profile, you seem to be a windows user. You can for example, create a new explorer context menu to your .yaml files (Add menu item to windows context menu only for specific filetype, Adding a context menu item in Windows for a specific file extension), and make it run a script that auto-fills your file.

    Here, an example python script called yamlfill404.py that would be used in the context call in a way like path/to/pythonexecutable/python.exe path/to/python/script/yamlfill404.py %1, where %1 is the path to the file being right clicked.

    Python file:

    import yaml
    from sys import argv
    import re
    
    order = ['openapi','paths','components']
    level0re = re.compile('(?<=\n)[^ ][^:]+')
    
    def _propfill(rootnode, nodes, value):
        if len(nodes) == 1:
            rootnode[nodes[0]] = value
        if len(nodes) > 1:
            nextnode = rootnode.get(nodes[0]) 
            if rootnode.get(nodes[0]) is None:
                nextnode = {}
                rootnode[nodes[0]] = nextnode
            _propfill(nextnode, nodes[1:], value)
    
    def propfill(rootnode, nodepath, value):
        _propfill(rootnode, [n.replace('__slash__','/') for n in nodepath.replace('\/','__slash__').split('/')], value)
    
    def yamlfill(filepath):
        with open(filepath, 'r') as file:
            yamltree = yaml.safe_load(file)
        #propfill(yamltree, 'components/schemas/notFoundResponse/...', '')
        propfill(yamltree, 'components/responses/notFound/description', 'Not found response')
        propfill(yamltree, 'components/responses/notFound/content/application\/json/schema/$ref', '#/components/schemas/notFoundResponse')
        responses = [mv['responses'] if 'responses' in mv else [] for pk,pv in (yamltree['paths'].items() if 'paths' in yamltree else []) for mk,mv in pv.items()]
        for response in responses:
            propfill(response, '404/$ref', '#/components/responses/notFound')
        yamlstring = yaml.dump(yamltree)
        offsets = [i[1] for i in sorted([(order.index(f.group(0)) if f.group(0) in order else len(order),f.start()-1) for f in [f for f in level0re.finditer('\n'+yamlstring)]])]
        offsets = [(offset,(sorted([o for o in offsets if o > offset]+[len(yamlstring)-1])[0])) for offset in offsets]
        with open(filepath[:-5]+'_404.yaml', 'w') as file:
            file.write(''.join(['\n'+yamlstring[o[0]:o[1]] for o in offsets]).strip())
    
    yamlfill(argv[-1])
    
    

    It processes the %1, which would be path/to/original.yaml and saves it as path/to/original_404.yaml (but you can change it to overwrite the original).

    This example script changes the yaml formating (quotes type, spacing, ordering etc), because of the library used pyyaml. I had to reorder the file with the order = ['openapi','paths','components'], because it loses ordering. For less instrusion, maybe a more manual insertion would be better suited. Maybe one that uses only regex. Maye using awk, there are plenty of ways.

    Unfortunately it is just a hack not not a solution.