Structure
The SOLR and Zookeeper Nodes run in a docker environment.
The Solr Authentication is a MultiAuthPlugin with the BasicAuthPlugin used to log in to the SOLR Admin UI and the JWTAuthPlugin used for the requests.
What I want
I need to send a request from my RestAPI to SOLR so I can search for content. For this I use pysolr.
The Problem
When I send the request with the token given as auth parameter I always get a "TypeError: 'str' object is not callable" error.
What I tried
I first tried my function without the authentication which works and returns my search results as expected. Then I tried with a static token which returns the error. And finally I tried the same with a always newly generated token which as well returns the error. Since I wasn't sure if the generated token works I printed the token in the console and tried to send a request with Postman with the given token which did work. When I use the "Basic Authentication" using a username and password it works as well.
What I expected
I expect, that the request is sent to SOLR. Then SOLR should check if the JWT token is valid with the settings set in the security.json and finally should return the search results.
Code
from typing import Any
import jwt
import pysolr
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi_cache.decorator import cache
from app.caching import TTL
from app.core.config import settings
from app.models import SearchResults
from app.queryparams import SearchQueryParams
from app.utils import check_search_input
router = APIRouter()
@router.get("/", response_model=SearchResults)
# @cache(expire=TTL.WEEK, namespace="public:search")
async def search(
skip: int = 0,
limit: int = 100,
params: SearchQueryParams = Depends(),
) -> Any:
"""
Search query.
"""
if not check_search_input(params=params):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Mindestens ein Feld muss ausgefüllt werden",
)
zookeeper = pysolr.ZooKeeper("search-zoo1,search-zoo2,search-zoo3")
jwt_token = jwt.encode({
"sub": "test-backend",
"name": "test-backend",
"iat": 1516239022,
"admin": False,
"iss": "test-solr-auth",
"solr": "test-backend",
"aud": "test-solr-api",
"exp": 33292598400,
"scope": "test-backend",
"realm": "test-solr-jwt"
}, settings.KEY, algorithm="RS256")
solr = pysolr.SolrCloud(zookeeper, "tag", auth=jwt_token)
res = solr.search(q=params.query, start=skip, rows=limit)
return SearchResults(data=res, count=res.hits)
Error traceback
INFO: "GET /v1/search/?skip=0&limit=100&query=title%3A%22Yellow%22&descending=false HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/app/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py", line 401, in run_asgi
result = await app( # type: ignore[func-returns-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
self.scope, self.receive, self.send
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/app/.venv/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.13/site-packages/fastapi/applications.py", line 1054, in __call__
await super().__call__(scope, receive, send)
File "/app/.venv/lib/python3.13/site-packages/starlette/applications.py", line 113, in __call__
await self.middleware_stack(scope, receive, send)
File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line 187, in __call__
raise exc
File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line 165, in __call__
await self.app(scope, receive, _send)
File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/cors.py", line 93, in __call__
await self.simple_response(scope, receive, send, request_headers=headers)
File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/cors.py", line 144, in simple_response
await self.app(scope, receive, send)
File "/app/.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "/app/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
raise exc
File "/app/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 715, in __call__
await self.middleware_stack(scope, receive, send)
File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 735, in app
await route.handle(scope, receive, send)
File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 288, in handle
await self.app(scope, receive, send)
File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 76, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "/app/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
raise exc
File "/app/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "/app/.venv/lib/python3.13/site-packages/starlette/routing.py", line 73, in app
response = await f(request)
^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.13/site-packages/fastapi/routing.py", line 301, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "/app/.venv/lib/python3.13/site-packages/fastapi/routing.py", line 212, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/app/api/routes/public/search.py", line 56, in search
res = solr.search(q=params.query, start=skip, rows=limit)
File "/app/.venv/lib/python3.13/site-packages/pysolr.py", line 834, in search
response = self._select(params, handler=search_handler)
File "/app/.venv/lib/python3.13/site-packages/pysolr.py", line 486, in _select
return self._send_request("get", path)
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
File "/app/.venv/lib/python3.13/site-packages/pysolr.py", line 1512, in _send_request
return Solr._send_request(self, method, path, body, headers, files)
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.13/site-packages/pysolr.py", line 414, in _send_request
resp = requests_method(
url,
...<4 lines>...
auth=self.auth,
)
File "/app/.venv/lib/python3.13/site-packages/requests/sessions.py", line 602, in get
return self.request("GET", url, **kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.13/site-packages/requests/sessions.py", line 575, in request
prep = self.prepare_request(req)
File "/app/.venv/lib/python3.13/site-packages/requests/sessions.py", line 484, in prepare_request
p.prepare(
~~~~~~~~~^
method=request.method.upper(),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<10 lines>...
hooks=merge_hooks(request.hooks, self.hooks),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/app/.venv/lib/python3.13/site-packages/requests/models.py", line 371, in prepare
self.prepare_auth(auth, url)
~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "/app/.venv/lib/python3.13/site-packages/requests/models.py", line 602, in prepare_auth
r = auth(self)
TypeError: 'str' object is not callable
So I found a solution to this problem. It might not be pretty but it works.
What I did is, I created a class named "JWTAuth" which uses the AuthBase class. When I call the class I pass the token. This way the auth parameter of pysolr will receive a object and not a string, thus it is happy.
class JWTAuth(AuthBase):
def __init__(self, jwt_token):
self.jwt_token = jwt_token
def __call__(self, r):
r.headers['Authorization'] = f'Bearer {self.jwt_token}'
return r
async def search(
skip: int = 0,
limit: int = 100,
params: SearchQueryParams = Depends(),
) -> Any:
"""
Search query.
"""
zookeeper = pysolr.ZooKeeper("search-zoo1,search-zoo2,search-zoo3")
solr = pysolr.SolrCloud(zookeeper, "tag", auth=JWTAuth(add-token-here))
res = solr.search(q=params.query, start=skip, rows=limit)
return SearchResults(data=res, count=res.hits)