on_ext_request authenticate request

54 views
Skip to first unread message

Moh. Sarip Hidayat

unread,
Dec 7, 2025, 5:16:23 AM (9 days ago) Dec 7
to Jam.py Users Mailing List
Hello,

According to the Jam.py documentation for `on_ext_request`, it explains how to process a request or retrieve data from another application or service. From what I understand, however, `on_ext_request` does not provide access to the client request headers.

In this discussion, Andrew Yushev mentioned that in version 6, `on_ext_request` "deprecated though it is supported"

I’m currently working on building an API within my Jam.py project. The main issue I’m facing is that I cannot expose the API publicly, since there’s no way to authenticate incoming requests (e.g., using an API key or JWT token).

Will version 7 address this limitation?

Reference:
Thank you!

Dean D. Babic

unread,
Dec 7, 2025, 8:09:29 PM (9 days ago) Dec 7
to Jam.py Users Mailing List
Lets see what the machine says:
https://chatgpt.com/share/693621fc-69a4-8012-ac25-a3d9c744a665

V7 has routing with on_request. Everyhing is going through on_request:

https://jampy-docs.readthedocs.io/projects/V7/en/latest/admin/routing.html

As seen, on_ext_request can be still used with V7.

D.

Message has been deleted
Message has been deleted
Message has been deleted

Dean D. Babic

unread,
Dec 7, 2025, 10:03:31 PM (9 days ago) Dec 7
to Jam.py Users Mailing List
Google has issues with your reply...
Screenshot 2025-12-08 110203.png

Moh. Sarip Hidayat

unread,
Dec 7, 2025, 10:07:48 PM (9 days ago) Dec 7
to Jam.py Users Mailing List
Hi Dean, thank you for your reply.

I tested what the machine says, and while `on_ext_request` is indeed can still be used, unfortunately the `request` object being passed is still only the ` request.path` part, same as how it is in v5.

The server module:

import sys

def on_ext_request(task, request, params):
    api_key = request.headers.get("X-API-Key")
    print(api_key, file=sys.stderr)
    return api_key


Curl command:

curl 'http://localhost:8080/ext/test' \
  -d "[]" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: secret123"


The response to the curl command:

{"result": {"data": [null, "'str' object has no attribute 'headers'"]}, "error": "'str' object has no attribute 'headers'"}

What appears in the log:

Traceback (most recent call last):
  File "/home/ubuntu/Projects/jam.py/v7/venv/lib/python3.12/site-packages/jam/wsgi.py", line 773, in on_ext
    data = self.task.on_ext_request(self.task, method, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "jam", line 4, in on_ext_request
    api_key = request.headers.get("X-API-Key")
              ^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'headers'
127.0.0.1 - - [08/Dec/2025 09:38:27] "POST /ext/test HTTP/1.1" 200 -


The sample project export is here. Please let me know if you need more details.

Thank you.

Best Regards,
Moh. Sarip Hidayat
Visit my profile website at musahi0128.github.io

P.S.: Sorry, just realized after trying to post multiple times that the zip file is not accepted.

Dean D. Babic

unread,
Dec 7, 2025, 11:02:12 PM (9 days ago) Dec 7
to Jam.py Users Mailing List
From here:
https://jampy-docs.readthedocs.io/projects/V7/en/latest/how_to/how_to_request_from_other_application.html


def on_ext_request(task, request, params):
print(request, params)
reqs = request.split('/')
if reqs[2] == 'bla':
print(params['key'])
return params['key']

curl  http://localhost:8080/ext/bla -d '{"key": "dhsagdhjgsdhsaghghg"}' -H "Content-Type: application/json"
{"result": {"status": 9, "data": "dhsagdhjgsdhsaghghg", "modification": 64}, "error": null}

In console:
127.0.0.1 - - [08/Dec/2025 11:58:40] "POST /ext/bla HTTP/1.1" 200 -
/ext/bla {'key': 'dhsagdhjgsdhsaghghg'}
dhsagdhjgsdhsaghghg

It is not the first time AI is wrong...

Moh. Sarip Hidayat

unread,
Dec 7, 2025, 11:36:34 PM (9 days ago) Dec 7
to Dean D. Babic, Jam.py Users Mailing List
Hi Dean, thank you for your reply.

I'm well aware of this approach but I don't think this is a proper way to do an API.

Here's what another "machine" says regarding this approach.

🔒 Security Concerns

  • Logging exposure: Request bodies are often logged by application servers, proxies, or debugging tools. This means your API key could end up in logs, making it easier to leak.
    → This we can control so no problem.

  • Caching risks: Some intermediaries (reverse proxies, CDNs) cache based on headers but not bodies. If the key is in the body, caching layers may inadvertently store sensitive data. 
    → Valid point, we might have no control over this.

  • Transport visibility: Security tools (like WAFs or API gateways) are typically designed to inspect headers for authentication. Keys in the body may bypass standard protections.
    → Another valid point.

📐 Standards & Best Practices

  • HTTP semantics: Authentication belongs in headers (Authorization: Bearer <token> or x-api-key: <key>). The body is meant for resource data, not credentials.

  • Consistency with OAuth/JWT: Widely adopted standards (OAuth2, JWT, API Gateway conventions) all use headers for tokens/keys. Deviating makes integration harder.

  • REST principles: The body should represent the resource being created/updated. Mixing authentication into it violates separation of concerns.

I know what to do (update Jam.py making what passed to on_ext_request is the full request object instead of only the request.path part) but unfortunately I'm not skilled enough therefore I don't know how to do it.

Thank you.

Best Regards,
Moh. Sarip Hidayat
Visit my profile website at musahi0128.github.io

--
You received this message because you are subscribed to a topic in the Google Groups "Jam.py Users Mailing List" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/jam-py/tG_S5fCK6i0/unsubscribe.
To unsubscribe from this group and all its topics, send an email to jam-py+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/jam-py/1b088aff-174b-4f7d-8c14-cec4e88897e5n%40googlegroups.com.

Dean D. Babic

unread,
Dec 8, 2025, 12:26:46 AM (9 days ago) Dec 8
to Jam.py Users Mailing List
Jam is working just fine with headers and OAuth, etc
through the on_request.

Otherwise, this would not work:
https://ipam2.pythonanywhere.com/

Jam.py v5 has no on_request.

Only v7 does. So yes, there is no on_ext_request  limitation  v7.

Dean D. Babic

unread,
Dec 8, 2025, 4:14:37 AM (8 days ago) Dec 8
to Jam.py Users Mailing List
See this?

curl  http://localhost:8080/ext/expenses -d '{"token": "dhsagdhjgsdhsaghghg"}' -H "Content-Type: application/json"
{"result": {"status": 9, "data": {"error": "Invalid token: Invalid token"}, "modification": 75}, "error": null}


curl  http://localhost:8080/ext/expenses -d '{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInJvbGVzIjpbInVzZXIiXSwiaWF0IjoxNzY1MTg0MDAwLCJleHAiOjE3NjUxODQ5MDAsInR5cGUiOiJhY2Nlc3MifQ.hKMT2tc_LrgtyzrpRwCxKzMeisGRJiIaOO7WkjhoWpM"}' -H "Content-Type: application/json"
{"result": {"status": 9, "data": {"request": "/ext/expenses", "params": {"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInJvbGVzIjpbInVzZXIiXSwiaWF0IjoxNzY1MTg0MDAwLCJleHAiOjE3NjUxODQ5MDAsInR5cGUiOiJhY2Nlc3MifQ.hKMT2tc_LrgtyzrpRwCxKzMeisGRJiIaOO7WkjhoWpM"}}, "modification": 75}, "error": null}

This is the server side token in to console:

127.0.0.1 - - [08/Dec/2025 16:53:18] "POST /api HTTP/1.1" 200 -
ACCESS: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInJvbGVzIjpbInVzZXIiXSwiaWF0IjoxNzY1MTg0MDAwLCJleHAiOjE3NjUxODQ5MDAsInR5cGUiOiJhY2Nlc3MifQ.hKMT2tc_LrgtyzrpRwCxKzMeisGRJiIaOO7WkjhoWpM
REFRESH: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsImlhdCI6MTc2NTE4NDAwMCwiZXhwIjoxNzY3Nzc2MDAwLCJ0eXBlIjoicmVmcmVzaCJ9.IccR3C_D6pHTH5A7L1IwHNLeqAdpjHoLV3-g1NXIUrk
User: alice
Roles: ['user']

You can't get it if not authenticated. Copy this code into server. 
This is all AI (pip install pyjwt cryptography).
Login again to App to get a new token after 15 mins:

curl  http://localhost:8080/ext/expenses -d '{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInJvbGVzIjpbInVzZXIiXSwiaWF0IjoxNzY1MTg0MDAwLCJleHAiOjE3NjUxODQ5MDAsInR5cGUiOiJhY2Nlc3MifQ.hKMT2tc_LrgtyzrpRwCxKzMeisGRJiIaOO7WkjhoWpM"}' -H "Content-Type: application/json"
{"result": {"status": 9, "data": {"error": "Invalid token: Token has expired"}, "modification": 78}, "error": null}



import jwt
import datetime
from typing import Dict, Any

class JWTError(Exception):
"""Base class for token errors."""


class JWTExpired(JWTError):
pass


class JWTInvalid(JWTError):
pass


def create_access_token(identity: str, roles: list[str] = None) -> str:
now = datetime.datetime.utcnow()
payload = {
"sub": identity,
"roles": roles or [],
"iat": now,
"exp": now + datetime.timedelta(minutes=ACCESS_TOKEN_LIFETIME),
"type": "access",
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGO)


def create_refresh_token(identity: str) -> str:
now = datetime.datetime.utcnow()
payload = {
"sub": identity,
"iat": now,
"exp": now + datetime.timedelta(days=REFRESH_TOKEN_LIFETIME),
"type": "refresh",
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGO)


def verify_token(token: str, expected_type: str = "access") -> Dict[str, Any]:
"""
Verifies token signature, expiration, and type.
Returns decoded payload if valid.
Raises JWTExpired or JWTInvalid.
"""
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGO])
except jwt.ExpiredSignatureError:
raise JWTExpired("Token has expired")
except jwt.PyJWTError:
raise JWTInvalid("Invalid token")

if payload.get("type") != expected_type:
raise JWTInvalid("Wrong token type")

return payload


# In production, load these from env variables
JWT_SECRET = "CHANGE_ME"
JWT_ALGO = "HS256"
ACCESS_TOKEN_LIFETIME = 15 # minutes
REFRESH_TOKEN_LIFETIME = 30 # days


access = create_access_token("alice", roles=["user"])
refresh = create_refresh_token("alice")

print("ACCESS:", access)
print("REFRESH:", refresh)

token = access # from request or header

try:
payload = verify_token(token, expected_type="access")
print("User:", payload["sub"])
print("Roles:", payload["roles"])
except JWTExpired:
print("Token expired")
except JWTInvalid as e:
print("Invalid token:", e)

def on_ext_request(task, request, params):
reqs = request.split('/')
if reqs[2] == 'expenses':

token = params['token']
if not token:
return {"error": "Missing token"}

# JWT verification
try:
payload = verify_token(token)
except Exception as e:
return {"error": f"Invalid token: {e}"}

return {"request": request, "params": params}


return {"error": "Unknown EXT endpoint"}




Pls contribute with at least 2-3 hours work. It is much more you "own" to the project tho....

Also, pls remove Google Analytics code from your app. 
I see all app analytics from VDR app (Indonesia). 

Thanks

r rad

unread,
Dec 8, 2025, 2:34:04 PM (8 days ago) Dec 8
to Jam.py Users Mailing List
I am writing this from memory and from general knowledge of applied technologies: 

- Jam.py is not a REST framework 
- Jam.py does not route except in very few later added more features than practice that it should 
- on_ext event is created for mutual communication of application processes, not necessarily on the same server 
- Jam.py does not publish server interfaces 
- Jam.py is not a server web framework 
- There is more, but basically it is an extremely powerful web system that has both a python and a javascript server that internally communicate with an AJAX interface, but not with REST calls, but something that looks like a function callback with a parameter list and a return string, which is a result or an error. And that those calls can be asynchronous. 
- Therefore you cannot get valid calls using REST tools and methodology. 
- Finally, on_ext can be a service, but with calls of type /ext/function_name(params_list). 
- The internal structure should be something like a match expression with many inline - lambda functions, which should be called with the appropriate routed input. 

PS. This is what I think, it doesn't have to be 100% correct. What do you think?

Salute, Radosv

Dean D. Babic

unread,
Dec 8, 2025, 10:26:18 PM (8 days ago) Dec 8
to Jam.py Users Mailing List
Hi Radosav, 

glad someone still remembers v5 !

However, v7 is REST and has routing!
Or, there would no be this (the expenses.html DOES NOT exist):
json_query_string_decode_jampy.png



Or with curl:
curl "http://127.0.0.1:8080/expenses.html?a=1&b=2"   -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc2NTIwMTY4NX0.CTb_E-4PqaPnzUh5PGtrTYMYwgZryAZy3eTrowbA8Qk"
{"user": "admin", "a": "1", "b": "2", "expenses": [{"id": 1, "amount": 100}, {"id": 2, "amount": 250}]}

 Which is the same result as the above pic.

Or if not authorized:

json_query_string_decode_02_jampy.png

All of the above is on_request event. 
 
So here is a REST Flask example:
There is no difference between Flask and Jam with the above. Of course, 
Flask has no on_request, and handles requests differently.

However, the developers just need to spend more time with v7
to get the full potential from on_request, routes, etc.

But will they is the million $ question...

D.


r rad

unread,
Dec 9, 2025, 8:27:23 AM (7 days ago) Dec 9
to Jam.py Users Mailing List
I can not say that it is clear for me?

Do you want to say that Jampy V7 can some as is 'http://example.com/user.search?name=rrad?
Or it must be in on_ext_request as part etc??? 

If only part "on_ext_request" do as REST is that mean Jampy V7 is REST?

Drazen Babic

unread,
Dec 9, 2025, 9:02:20 AM (7 days ago) Dec 9
to r rad, Jam.py Users Mailing List
Radosav,

what u see is NOT on_ext_request.

It is on_request.

Massive diference called routing.

Routing (OAuth), info was posted on December 2023:


Cross domain Auth with SAML was posted even before that:
Both was/is showing that routing, tokens, etc work.

One implements REST through routing in V7.
Just like Flask/Django etc

Below examples with curl are proving that one can get auth token from the App generated JWT, and use it to authenticate on API request.
No token, no authorisation. 

How is this implemened is up to a developer.

Regards

D.


--
You received this message because you are subscribed to the Google Groups "Jam.py Users Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jam-py+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/jam-py/e3a704b6-5f74-44d9-9887-5c6e4b3dbf7en%40googlegroups.com.

Drazen Babic

unread,
Dec 9, 2025, 9:18:24 AM (7 days ago) Dec 9
to r rad, Jam.py Users Mailing List
All routing is showed in here:


You can download the Export and look for Task/Server, which has a code in here:


All OAuth, SAML, REST, routing, etc can "live" in on_request, which handles ALL requests to Jam.
on_request is a "mother" of Jam.py v7.

Mike drop. 



--
You received this message because you are subscribed to the Google Groups "Jam.py Users Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jam-py+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/jam-py/e3a704b6-5f74-44d9-9887-5c6e4b3dbf7en%40googlegroups.com.

Danijel Kaurin

unread,
Dec 9, 2025, 2:57:46 PM (7 days ago) Dec 9
to Moh. Sarip Hidayat, Dean D. Babic, Jam.py Users Mailing List
Hi.

In V7, on_ext_request is outdated and on_request is an advanced version of the handler function where you can get the request header. 

But we didn't test it well jet so you can add this part of code to the wsgy.py to get headers with on_ext_request:

Screenshot 2025-08-26 195010.png

Regards

Danijel

You received this message because you are subscribed to the Google Groups "Jam.py Users Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jam-py+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/jam-py/CAF0JkBpVVwqXtdybW7hmJoobuwXNe8D6YnCAjGoUFpqh3YsZRA%40mail.gmail.com.

Danijel Kaurin

unread,
Dec 9, 2025, 3:55:06 PM (7 days ago) Dec 9
to Moh. Sarip Hidayat, Dean D. Babic, Jam.py Users Mailing List
Here is how to get request headers with on_request event handlers:

Request curl:

postman request POST 'http://192.168.1.2:8080/test_test' \
  --header 'api-key: 122448' \
  --header 'Content-Type: application/json' \
  --body '{
    "mjesto": "75000",
    "adresa": "1 tuzlanske brigade",
    "sifraKomitenta": "0"
}'
....

server code:

def on_request(task, request):
    from jam.wsgi import Response
    from werkzeug.exceptions import MethodNotAllowed

    parts = request.path.strip('/').split('/')
   
    if not parts[0]:
        if task.logged_in(request):
            return task.serve_page('index.html')
        else:
            return task.redirect('/login.html')

    elif parts[0] == 'test_test':
        if request.method == 'POST':
            print(request.method )
            print(request.headers)
            api_key = request.headers['Api-key']
               
            print(api_key)
            return Response('Succesfull test!')
        if request.method == 'GET':
            raise MethodNotAllowed()

Key part is to import: from jam.wsgi import Response to return response without raising error. Always wrap response in return Response(your_response) because wsgi.py expects that.
Now you can use full power of the Jam.py V7!

Happy coding!
Regards

Danijel

Reply all
Reply to author
Forward
0 new messages