Repository: tiangolo/fastapi

Issue:
OpenAPI: response keys are limited to be status codes
**Describe the bug**
According to OpenAPI 3 specification, under `responses` dictionary each definition starts with the keys which is either a valid status codes (200, 204, 404, etc.), a [range of codes](https://swagger.io/docs/specification/describing-responses#status-codes) (1XX, 2XX, 3XX, etc.) or the word ["default"](https://swagger.io/docs/specification/describing-responses#default).

If a path is defined with such response, the server crashes when opening the doc, redoc or openapi.json URLs.

**To Reproduce**
```python
from fastapi import FastAPI, HTTPException
from starlette import status
from . import schemas

app = FastAPI()

@app.get('/customer/{id}',
            status_code=status.HTTP_200_OK,
            response_model=schemas.CustomerDetailsResponse,
            responses={
                '4xx': {'model': schemas.ErrorResponse},
                '5xx': {'model': schemas.ErrorResponse},
            },
            )
def customer_details(id: str):
    raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED)
```

**Expected behavior**
The openapi.json is created, documentation opens, and the responses show the range / default 
definitions.

```yaml
# ...
"/api/customer/{id}":
  get:
    responses:
      '200':
        description: Successful Response
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CustomerDetailsResponse"
      4xx:
        description: Additional Response
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ErrorResponse"
      5xx:
        description: Additional Response
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ErrorResponse"
    summary: Customer Details
    operationId: customer_details_api_customer__id__get
    parameters:
    - required: true
      schema:
        title: Id
        type: string
      name: id
      in: path
```

**Environment:**
 - OS: masOS 10.14.6
 - Python: 3.6.5
 - FastAPI: 0.35.0
OpenAPI: response keys are limited to be status codes
**Describe the bug**
According to OpenAPI 3 specification, under `responses` dictionary each definition starts with the keys which is either a valid status codes (200, 204, 404, etc.), a [range of codes](https://swagger.io/docs/specification/describing-responses#status-codes) (1XX, 2XX, 3XX, etc.) or the word ["default"](https://swagger.io/docs/specification/describing-responses#default).

If a path is defined with such response, the server crashes when opening the doc, redoc or openapi.json URLs.

**To Reproduce**
```python
from fastapi import FastAPI, HTTPException
from starlette import status
from . import schemas

app = FastAPI()

@app.get('/customer/{id}',
            status_code=status.HTTP_200_OK,
            response_model=schemas.CustomerDetailsResponse,
            responses={
                '4xx': {'model': schemas.ErrorResponse},
                '5xx': {'model': schemas.ErrorResponse},
            },
            )
def customer_details(id: str):
    raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED)
```

**Expected behavior**
The openapi.json is created, documentation opens, and the responses show the range / default 
definitions.

```yaml
# ...
"/api/customer/{id}":
  get:
    responses:
      '200':
        description: Successful Response
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CustomerDetailsResponse"
      4xx:
        description: Additional Response
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ErrorResponse"
      5xx:
        description: Additional Response
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ErrorResponse"
    summary: Customer Details
    operationId: customer_details_api_customer__id__get
    parameters:
    - required: true
      schema:
        title: Id
        type: string
      name: id
      in: path
```

**Environment:**
 - OS: masOS 10.14.6
 - Python: 3.6.5
 - FastAPI: 0.35.0


Discussion:

Resolving patch:
diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py
--- a/fastapi/openapi/utils.py
+++ b/fastapi/openapi/utils.py
@@ -43,6 +43,15 @@
     },
 }
 
+status_code_ranges: Dict[str, str] = {
+    "1XX": "Information",
+    "2XX": "Success",
+    "3XX": "Redirection",
+    "4XX": "Client Error",
+    "5XX": "Server Error",
+    "default": "Default Response",
+}
+
 
 def get_openapi_params(dependant: Dependant) -> List[Field]:
     flat_dependant = get_flat_dependant(dependant)
@@ -190,12 +199,14 @@ def get_openapi_path(
                         response.setdefault("content", {}).setdefault(
                             "application/json", {}
                         )["schema"] = response_schema
-                    status_text = http.client.responses.get(int(additional_status_code))
+                    status_text: Optional[str] = status_code_ranges.get(
+                        str(additional_status_code).upper()
+                    ) or http.client.responses.get(int(additional_status_code))
                     response.setdefault(
                         "description", status_text or "Additional Response"
                     )
                     operation.setdefault("responses", {})[
-                        str(additional_status_code)
+                        str(additional_status_code).upper()
                     ] = response
             status_code = str(route.status_code)
             response_schema = {"type": "string"}


Test patch:
diff --git a/tests/test_additional_responses_bad.py b/tests/test_additional_responses_bad.py
new file mode 100644
--- /dev/null
+++ b/tests/test_additional_responses_bad.py
@@ -0,0 +1,40 @@
+import pytest
+from fastapi import FastAPI
+from starlette.testclient import TestClient
+
+app = FastAPI()
+
+
+@app.get("/a", responses={"hello": {"description": "Not a valid additional response"}})
+async def a():
+    pass  # pragma: no cover
+
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "Fast API", "version": "0.1.0"},
+    "paths": {
+        "/a": {
+            "get": {
+                "responses": {
+                    # this is how one would imagine the openapi schema to be
+                    # but since the key is not valid, openapi.utils.get_openapi will raise ValueError
+                    "hello": {"description": "Not a valid additional response"},
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                },
+                "summary": "A",
+                "operationId": "a_a_get",
+            }
+        }
+    },
+}
+
+client = TestClient(app)
+
+
+def test_openapi_schema():
+    with pytest.raises(ValueError):
+        client.get("/openapi.json")
diff --git a/tests/test_additional_responses_router.py b/tests/test_additional_responses_router.py
--- a/tests/test_additional_responses_router.py
+++ b/tests/test_additional_responses_router.py
@@ -10,12 +10,24 @@ async def a():
     return "a"
 
 
-@router.get("/b", responses={502: {"description": "Error 2"}})
+@router.get(
+    "/b",
+    responses={
+        502: {"description": "Error 2"},
+        "4XX": {"description": "Error with range, upper"},
+    },
+)
 async def b():
     return "b"
 
 
-@router.get("/c", responses={501: {"description": "Error 3"}})
+@router.get(
+    "/c",
+    responses={
+        "400": {"description": "Error with str"},
+        "5xx": {"description": "Error with range, lower"},
+    },
+)
 async def c():
     return "c"
 
@@ -43,6 +55,7 @@ async def c():
             "get": {
                 "responses": {
                     "502": {"description": "Error 2"},
+                    "4XX": {"description": "Error with range, upper"},
                     "200": {
                         "description": "Successful Response",
                         "content": {"application/json": {"schema": {}}},
@@ -55,7 +68,8 @@ async def c():
         "/c": {
             "get": {
                 "responses": {
-                    "501": {"description": "Error 3"},
+                    "400": {"description": "Error with str"},
+                    "5XX": {"description": "Error with range, lower"},
                     "200": {
                         "description": "Successful Response",
                         "content": {"application/json": {"schema": {}}},