Repository: apache/airflow

Issue:
API server /plugin crashes
### Apache Airflow version

2.3.3 (latest released)

### What happened

The `/plugins` endpoint returned a 500 http status code.

```
curl -X GET http://localhost:8080/api/v1/plugins\?limit\=1  \
   -H 'Cache-Control: no-cache' \
   --user "admin:admin"
{
  "detail": "\"{'name': 'Test View', 'category': 'Test Plugin', 'view': 'test.appbuilder_views.TestAppBuilderBaseView'}\" is not of type 'object'\n\nFailed validating 'type' in schema['allOf'][0]['properties']['plugins']['items']['properties']['appbuilder_views']['items']:\n    {'nullable': True, 'type': 'object'}\n\nOn instance['plugins'][0]['appbuilder_views'][0]:\n    (\"{'name': 'Test View', 'category': 'Test Plugin', 'view': \"\n     \"'test.appbuilder_views.TestAppBuilderBaseView'}\")",
  "status": 500,
  "title": "Response body does not conform to specification",
  "type": "http://apache-airflow-docs.s3-website.eu-central-1.amazonaws.com/docs/apache-airflow/latest/stable-rest-api-ref.html#section/Errors/Unknown"
}
```

The error message in the webserver is as followed

```
[2022-08-03 17:07:57,705] {validation.py:244} ERROR - http://localhost:8080/api/v1/plugins?limit=1 validation error: "{'name': 'Test View', 'category': 'Test Plugin', 'view': 'test.appbuilder_views.TestAppBuilderBaseView'}" is not of type 'object'

Failed validating 'type' in schema['allOf'][0]['properties']['plugins']['items']['properties']['appbuilder_views']['items']:
    {'nullable': True, 'type': 'object'}

On instance['plugins'][0]['appbuilder_views'][0]:
    ("{'name': 'Test View', 'category': 'Test Plugin', 'view': "
     "'test.appbuilder_views.TestAppBuilderBaseView'}")
172.18.0.1 - admin [03/Aug/2022:17:10:17 +0000] "GET /api/v1/plugins?limit=1 HTTP/1.1" 500 733 "-" "curl/7.79.1"
```

### What you think should happen instead

The response should contain all the plugins integrated with Airflow.

### How to reproduce

Create a simple plugin in the plugin directory.

`appbuilder_views.py`

```
from flask_appbuilder import expose, BaseView as AppBuilderBaseView


# Creating a flask appbuilder BaseView
class TestAppBuilderBaseView(AppBuilderBaseView):
    @expose("/")
    def test(self):
        return self.render_template("test_plugin/test.html", content="Hello galaxy!")

```

`plugin.py`

```
from airflow.plugins_manager import AirflowPlugin
from test.appbuilder_views import TestAppBuilderBaseView


class TestPlugin(AirflowPlugin):
    name = "test"

    appbuilder_views = [
        {
            "name": "Test View",
            "category": "Test Plugin",
            "view": TestAppBuilderBaseView()
        }
    ]

```

Call the `/plugin` endpoint.

```
curl -X GET http://localhost:8080/api/v1/plugins\?limit\=1  \
   -H 'Cache-Control: no-cache' \
   --user "admin:admin"
```

### Operating System

N/A

### Versions of Apache Airflow Providers

_No response_

### Deployment

Astronomer

### Deployment details

_No response_

### Anything else

_No response_

### Are you willing to submit PR?

- [X] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)



Discussion:
You need to take a look at the error logs of webserver - there is not enough details (and rightfully so) in response returned to the client.
The error log of the webserver is the same as the detail in the api response.

I have updated the issue with the error in the webserver.
Looks like something’s over-serialising the response. Things in `appbuilder_views` should be a dict (“object” in JSON Schema terminology) but the validator got a serialised JSON string.
Oh it’s the other way around, each item in `appbuilder_views` is a dict, but the schema is incorrectly defined to expect a str.

Resolving patch:
diff --git a/airflow/api_connexion/endpoints/plugin_endpoint.py b/airflow/api_connexion/endpoints/plugin_endpoint.py
--- a/airflow/api_connexion/endpoints/plugin_endpoint.py
+++ b/airflow/api_connexion/endpoints/plugin_endpoint.py
@@ -27,7 +27,5 @@
 def get_plugins(*, limit: int, offset: int = 0) -> APIResponse:
     """Get plugins endpoint"""
     plugins_info = get_plugin_info()
-    total_entries = len(plugins_info)
-    plugins_info = plugins_info[offset:]
-    plugins_info = plugins_info[:limit]
-    return plugin_collection_schema.dump(PluginCollection(plugins=plugins_info, total_entries=total_entries))
+    collection = PluginCollection(plugins=plugins_info[offset:][:limit], total_entries=len(plugins_info))
+    return plugin_collection_schema.dump(collection)
diff --git a/airflow/api_connexion/schemas/plugin_schema.py b/airflow/api_connexion/schemas/plugin_schema.py
--- a/airflow/api_connexion/schemas/plugin_schema.py
+++ b/airflow/api_connexion/schemas/plugin_schema.py
@@ -23,16 +23,15 @@
 class PluginSchema(Schema):
     """Plugin schema"""
 
-    number = fields.Int()
     name = fields.String()
     hooks = fields.List(fields.String())
     executors = fields.List(fields.String())
-    macros = fields.List(fields.String())
-    flask_blueprints = fields.List(fields.String())
-    appbuilder_views = fields.List(fields.String())
+    macros = fields.List(fields.Dict())
+    flask_blueprints = fields.List(fields.Dict())
+    appbuilder_views = fields.List(fields.Dict())
     appbuilder_menu_items = fields.List(fields.Dict())
-    global_operator_extra_links = fields.List(fields.String())
-    operator_extra_links = fields.List(fields.String())
+    global_operator_extra_links = fields.List(fields.Dict())
+    operator_extra_links = fields.List(fields.Dict())
     source = fields.String()
 
 


Test patch: