-
This App uses Standard Frappe config files
site_config.jsonfor site specific settings andcommon_site_config.jsonfor value shared across all sites. Do no put passwords, API Keys or Other secrets intocommon_site_config.json, because it is often commited or copied across environments and expose sensitive data to every site using bench. If a secret is stored there by mistake, it can break security, cause developments to fail, and make it hard to keep different sites ceredentials isolated -
When you run
bench start, it launches four processes:webfor HTTP requests,workerfor background jobs,schedulerfor scheduled tasks, andsocketiofor websocket and realtime notifications. If theworkerprocess crashes, queued background jobs stop being processed until it is restarted. Any jobs already running may fail or remain in an incomplete state, and new jobs will accumulate in the queue. This can delay email sending, reports, and other asynchronous work until the worker is back online.
-
Frappe finds it by parsing the URL to extract the app name (quickfix), module name (api), and method name (get_job_summary), then dynamically importing the module and calling the function with the request data. If the function doesn't exist, Frappe will raise an ImportError or AttributeError.
-
When a browser hits /api/resource/Job Card/JC-2024-0001, Frappe treats it as a request to perform CRUD operations (Create, Read, Update, Delete) on a specific document of the "Job Card" DocType with the name "JC-2024-0001". This goes through Frappe's built-in ORM layer, which handles permissions, field validations, hooks, and database interactions automatically, without needing custom code.
-
When a browser hits /track-job, Frappe routes it to a custom page defined in the app's templates/pages/track-job/ directory. The handler is typically the track-job.py file (containing a get_context function for server-side data preparation) paired with track-job.html for the template, or just the HTML file for static content.
- The X-Frappe-CSRF-Token value in a POST request comes from the csrf_token cookie that Frappe sets in the browser when a user logs in or visits the site. This token is generated server-side using Frappe's session management and cryptography utilities, ensuring it's unique per session to prevent replay attacks.
If you omit the X-Frappe-CSRF-Token header from a POST request, Frappe's CSRF protection middleware will reject it with a 403 Forbidden error, as it verifies the token against the session to block potential cross-site request forgery attempts. This is enforced in frappe.csrf.validate_csrf() or similar core functions.
- frappe.session.data is a dictionary containing session-specific information for the current user in Frappe. It typically includes keys like 'user' (the logged-in username), 'csrf_token' (for CSRF protection), 'sid' (session ID), 'full_name', 'user_type', and other session metadata such as 'last_updated' or 'ip_address'. This data is stored in the session store (usually Redis or database) and is used to maintain user state across requests, enforce permissions, and secure operations. If no user is logged in, it may be empty or contain default values.
- With developer_mode: 1 enabled in site_config.json, triggering a Python exception in a whitelisted method (decorated with @frappe.whitelist()) causes Frappe to return a detailed error response to the browser instead of a sanitized one.
The browser receives a JSON object like:
{
"exc": "Traceback (most recent call last):\n File \"...\", line ..., in ...\n raise Exception('Test error')\nException: Test error",
"exc_type": "Exception",
"message": "Test error",
"_server_messages": "[\"Test error\"]"
}
- With developer_mode: 0 (production mode), triggering a Python exception in a whitelisted method causes Frappe to return a generic sanitized error response to the browser:
{
"message": "Internal Server Error",
"exc": null,
"exc_type": null,
"_server_messages": ["An error occurred"]
}
- In production mode with developer_mode: 0, errors are logged server-side in several places:
-
Frappe Error Log – Stored in the database under the Error Log DocType, viewable by administrators in the Frappe UI at /app/error-log
-
System logs – Written to disk at logs/ directory (e.g., error.log, bench.log)
-
Error email notifications – If configured in site_config.json, emails are sent to specified admins with full error details
- When you call frappe.get_doc("Job Card", name) without ignore_permissions=True and the logged-in QF Technician user lacks read permission for that Job Card, Frappe raises:
This error is raised at the ORM layer (in frappe/model/document.py) during the document loading process, before any of your method logic executes. The permission check happens inside frappe.get_doc() itself.
From an API perspective, the request is stopped at the whitelist handler layer — the error bubbles up from get_doc(), is caught by the /api/method/ handler, and is returned to the browser as:
{
"message": "User does not have permission to read",
"exc_type": "PermissionError"
}
- I ran the query and found:
- tabScheduled Job Log
- tabScheduled Job Type
In Frappe, the database table convention is to prefix document tables with tab, so a DocType named Job Card maps to tabJob Card. This makes it easy to distinguish DocType tables from other tables and is part of Frappe’s ORM convention for document storage.
Calling self.save() inside an on_update() hook causes infinite recursion because the save() method triggers all on_update hooks, including the one that called it. This leads to a RecursionError: maximum recursion depth exceeded and can crash the application.
Issues:
- Infinite Loop: Each
save()call re-triggerson_update, creating an endless cycle. - Performance Degradation: Consumes stack space rapidly, leading to stack overflow.
- Data Corruption Risk: If the hook modifies data, repeated saves can overwrite changes unexpectedly.
- Hard to Debug: The error occurs deep in the call stack, making it difficult to trace back to the hook.
Corrected Pattern:
Use a flag to prevent re-entry into the hook. Set a temporary attribute (e.g., self._updating_status = True) before performing updates, and check it at the start. Reset it after the operation. This ensures the hook runs only once per save cycle.
Example:
def on_update(self):
if not getattr(self, '_updating_status', False):
self._updating_status = True
# Perform updates here
self._updating_status = FalseThis pattern allows controlled updates without recursion while maintaining hook functionality.
I registered two validate handlers for Job Card in this app:
- Controller handler:
quickfix.quickfix.doctype.job_card.job_card.JobCard.validate() doc_eventshandler:quickfix.job_card_event_demo.job_card_validate_doc_event
Frappe runs them in this order:
- The controller method (
validate()on the document class, including anysuper()chain fromoverride_doctype_class) - The
doc_eventshandlers for that exact DocType - The
doc_eventshandlers registered under"*"
This order comes from Document.run_method() and Document.hook() in Frappe core. run_method() calls the controller method first, and only if it completes does it continue into hook handlers. Inside Document.hook(), Frappe iterates:
doc_events.get(self.doctype, {}).get(method, []) + doc_events.get("*", {}).get(method, [])So specific DocType hooks are evaluated before wildcard hooks.
Only the first one that executes gets a chance to raise.
- If the controller
validate()raisesfrappe.ValidationError, execution stops immediately and thedoc_eventsvalidate hook does not run. - If the controller succeeds and the
doc_eventsvalidate hook raises, the save fails at that point.
So if both are coded to raise for the same save, the controller error "wins" because it runs first.
Yes, both run.
For the same event, Frappe runs:
- the specific DocType handler
- the wildcard (
"*") handler
In the test/demo for Job Card.validate, the observed order is:
controller -> specific -> wildcard
That means:
- the controller still runs before all
doc_events - both
doc_eventshooks execute - the specific hook runs before the wildcard hook