| name | dmvcframework-jsonrpc |
| description | Use when building or consuming a JSON-RPC 2.0 endpoint with DelphiMVCFramework โ publishing a plain Delphi class as remotely callable methods, or calling one from a Delphi client. Triggers on "JSON-RPC", "JSONRPC", "MVCFramework.JSONRPC", "RPC method", "PublishObject", "TMVCJSONRPCController", "TMVCJSONRPCPublisher", "IMVCJSONRPCExecutor", "TMVCJSONRPCExecutor", "TJSONRPCRequest", "IJSONRPCResponse", "MVCJSONRPCAllowGET", "EMVCJSONRPCError", "JSON-RPC notification", "the wizard's JSON-RPC Service preset". |
DMVCFramework โ JSON-RPC 2.0
A JSON-RPC endpoint is a plain Delphi class published on a URL segment. There are no routing attributes
per method, no [MVCPath]: the framework reflects over the class with RTTI and exposes its public methods.
Everything below is copied from sources/MVCFramework.JSONRPC.pas,
sources/MVCFramework.JSONRPC.Client.pas, the samples/jsonrpc/ projects and the wizard's generated
JSONRPCServiceU.pas. Do not invent names โ this API does not follow JSON-RPC library spellings from
other ecosystems (there is no RegisterMethod, no [JsonRpcMethod], no AddRpcHandler).
Reference files โ read the one you need
| File | Read it when the task involves |
|---|
reference/client.md | Calling a JSON-RPC endpoint from Delphi: IMVCJSONRPCExecutor / TMVCJSONRPCExecutor, ExecuteRequest / ExecuteNotification and their async twins, building an IJSONRPCRequest, reading IJSONRPCResponse, client-side ownership, headers/TLS/tracing |
When in doubt about an API โ verify it, never guess
Never invent an identifier or answer from memory. If you need a signature this skill does not cover, ask the
user for the path to their DelphiMVCFramework checkout and read sources/ (and the matching samples/
project); failing that, read the official repository โ
sources ยท
samples.
If you still cannot verify it, say so. Where a skill and a sample disagree, the sample wins.
Delphi Language Target
Delphi 11 Alexandria or later โ inline var declarations and for var loops are fine, and this skill's
examples use them. Do not use Delphi 13 Florence-only syntax (NameOf, inline if expressions).
For the language and the RTL themselves โ version gating, lifetime, strings, generics, threading โ load the
delphi skill.
REQUIRED REFERENCE โ dmvcframework-security. A JSON-RPC endpoint is a single URL that dispatches to
arbitrary methods by name from the request body โ every public method on the published class is reachable
by anyone who can reach the endpoint. Follow that skill for access control, mass assignment, SQL injection,
JWT and secrets before shipping. Invoke it whenever you write or review an RPC method โ not only when the
user says "security".
0. STOP โ start from a wizard project
Never create the project from scratch. Never hand-write the .dpr bootstrap.
The IDE wizard ships a JSON-RPC Service preset (DMVC.Expert.Presets.pas, ppJSONRPC) that generates a
compiling project with the endpoint already published. Your job is to add methods to it.
Step 1 โ detect it. The user is expected to have run the wizard and started the agent from inside the
project folder. A JSON-RPC wizard project has:
*.dpr Boot + RunServer (Indy Direct by default)
BootConfigU.pas dotEnv + LoggerPro
EngineConfigU.pas ConfigureEngine โ AEngine.PublishObject(...) lives HERE
JSONRPCServiceU.pas TMyJSONRPCService โ YOUR RPC METHODS GO HERE
Controllers.HomeU.pas health-check controller
bin/.env port and settings
Read JSONRPCServiceU.pas and EngineConfigU.pas before writing anything, and follow their conventions.
Step 2 โ if there is no wizard project, stop. Do not scaffold one. Tell the user:
This skill works on a project generated by the DMVCFramework IDE wizard, and I do not see one here.
Please create it first:
Delphi IDE โ File โ New โ Other โ Delphi Projects โ DelphiMVCFramework โ New DMVCFramework Application
Pick the JSON-RPC Service preset. Accept the defaults โ the server backend is Indy Direct. Compile
and run it once. Then cd into the project folder, start me there, and tell me which RPC methods to add.
Then wait.
The host may be WebBroker (ISAPI, Apache) โ that is fine. The samples/jsonrpc/jsonrpcserver project is
WebBroker-hosted and publishes the object from WebModuleCreate. Keep the host, do not migrate it, do not
suggest migrating it. Everything above the host is identical on every backend. See dmvcframework,
reference/servers.md.
1. Publishing a class โ PublishObject
TMVCEngine.PublishObject is the registration method (sources/MVCFramework.pas):
function PublishObject(const AObjectCreatorDelegate: TMVCObjectCreatorDelegate;
const AURLSegment: string;
ExceptionHandler: TMVCJSONRPCExceptionHandlerProc = nil): TMVCEngine;
TMVCObjectCreatorDelegate = reference to function: TObject; // MVCFramework.pas
In a wizard project it goes in ConfigureEngine, in EngineConfigU.pas:
procedure ConfigureEngine(AEngine: TMVCEngine);
begin
// Controllers
AEngine.AddController(THomeController);
// Controllers - END
// Middleware
// Middleware - END
AEngine.PublishObject(
function: TObject
begin
Result := TMyJSONRPCService.Create;
end, '/jsonrpc');
end;
- The delegate runs once per request โ a fresh instance per call, so per-request state in fields is safe.
- The framework owns and frees the instance it created (
PublishObject passes Owns = True to
TMVCJSONRPCPublisher.Create). Do not keep a reference and free it yourself.
PublishObject returns the engine, so calls chain. You can publish several classes on several segments.
- Under the hood it is
AddController(TMVCJSONRPCPublisher, <delegate>, AURLSegment) โ a JSON-RPC endpoint
is a controller, so every middleware in the chain (CORS, compression, JWT, ActiveRecord) applies to it.
Alternative: descend from TMVCJSONRPCController
type
TMyRPC = class(TMVCJSONRPCController)
public
function Subtract(Value1, Value2: Int64): Integer;
end;
AEngine.AddController(TMyRPC, '/jsonrpc');
Use this when the RPC methods need the request context: TMVCJSONRPCController descends from
TMVCController, so Context is available directly. Methods declared on that class are invokable;
methods inherited from an ancestor are only invokable if marked [MVCInheritable]
(MVCInheritableAttribute, declared in MVCFramework).
The wizard uses PublishObject with a plain class. Prefer that unless you need Context.
2. What becomes an RPC method
TMVCJSONRPCController.CanBeRemotelyInvoked is the whole rule:
Result := (RTTIMethod.Visibility = mvPublic) and (RTTIMethod.MethodKind in [mkProcedure, mkFunction]);
Result := Result and not IsReservedMethodName(RTTIMethod.Name);
| Rule | Detail |
|---|
| Visibility | public only. published is mvPublished, not mvPublic โ a published method is not callable. private/protected/strict private are not callable either. |
| Kind | function or procedure. Constructors, destructors and properties are not callable. |
| Reserved names | OnBeforeRoutingHook, OnBeforeCallHook, OnAfterCallHook can never be invoked remotely. |
| Name matching | Case-insensitive (SameText). The samples send 'subtract' for function Subtract. |
| Overloads | The lookup returns the first match by name. Do not overload an invokable method. |
function vs procedure | Not cosmetic โ see ยง5. A function can only be called as a request; a procedure only as a notification. |
[MVCDoc('...')] on a method is picked up by the /describe endpoint (ยง9). It is documentation only.
[MVCJSONRPCAllowGET] on a method also allows the call over HTTP GET; without it, GET is rejected with
-32600 Invalid Request / "Method callable with POST only". The check reads the method's own attributes.
3. Parameters
Named or positional โ both work
The framework inspects params in the request: a JSON array โ positional, a JSON object โ named.
Named params are matched against the Delphi formal parameter name, case-insensitively. Rename a
parameter and every named-params client breaks.
function Subtract(Value1, Value2: Integer): Integer;
{"jsonrpc":"2.0","method":"subtract","params":[10,3],"id":1}
{"jsonrpc":"2.0","method":"subtract","params":{"Value1":10,"Value2":3},"id":1}
Counts are checked: positional must match exactly; named may not exceed the declared count. A missing named
param raises Invalid params unless the parameter carries [MVCJSONRPCOptional], in which case the
type's zero value ('', 0, 0.0, False, nil) is passed. Two caveats, both verified in
InvokeMethod:
[MVCJSONRPCOptional] is honoured only on the named-params path. With a positional array the count
must still match exactly.
- The value passed is the framework's zero value. A Delphi default in the declaration (
= False,
= 'INFO') is not consulted โ RTTI invocation does not apply Delphi defaults.
Parameter modifiers
Only const and plain by-value are allowed. The framework rejects the pfVar, pfOut and pfArray
flags โ i.e. var, out and open-array (array of โฆ) parameters โ with EMVCJSONRPCInvalidParams at call
time: "Parameter modifier not supported for formal parameter [...]. Only const and value modifiers are
allowed." A TArray<T> parameter is a normal dynamic array and is fine.
Supported parameter types
Verified in JSONDataValueToTValueParamEx (MVCFramework.JSONRPC.pas):
| Delphi type | JSON |
|---|
string | string (a non-string raises Invalid params) |
Integer, Int64 | number |
Double, Extended, Single | number (an integer literal is accepted) |
Boolean | true/false |
TDate, TTime, TDateTime | ISO-8601 string |
| enum | its name as a string (or a bool for Boolean-kind) |
| set | comma-separated string of element names |
TJDOJsonObject / TJDOJsonArray | object / array โ passed as a clone |
TObjectList<T> and other duck-typed lists | array of objects |
any other TObject descendant | object, deserialized into a newly created instance |
record | object |
dynamic array / TArray<record> | array |
Ownership of parameters โ the framework frees them
After the call, InvokeMethod walks the parameter array and frees every parameter that is an object
(and FreeMems every record). Never free an object parameter inside an RPC method, and never store it
past the end of the call โ take a copy if you need to keep it.
function TMyJSONRPCService.SavePerson(const Person: TPerson): Integer;
begin
Result := fRepo.Save(Person); // correct โ do NOT Person.Free
end;
Returning the parameter object as the result is also wrong: the result is freed too (ยง4) โ double free.
Injecting services โ [MVCInject]
A parameter marked [MVCInject] is not read from params; it is resolved from the service container and
does not count towards the parameter count. Its type must be an interface.
function GetCustomer(const ID: Integer;
[MVCInject] CustomerService: ICustomerService): TCustomer;
[MVCInject('name')] selects a named registration (MVCInjectAttribute.ServiceName). See dmvcframework,
reference/di-and-repository.md.
Collecting extra params โ [MVCJSONRPCRestParams]
On the last parameter, which must be TJDOJsonArray: every parameter the client sent that the signature
does not declare is collected into that array, each item an object with "value" and (for named params)
"name". Written for protocols like MCP that add fields such as _meta. Without it, extra params are an
error.
4. Return values and ownership
The result of a function is serialized into the result member by
TMVCJsonDataObjectsSerializer.TValueToJSONObjectProperty โ the same serializer that renders controller
actions. Scalars, strings, TDate/TTime/TDateTime, enums, sets, nullables from MVCFramework.Nullables,
records and arrays of records all work. TDataSet becomes a JSON array of rows; TObjectList<T> becomes a
JSON array; TJDOJsonObject/TJDOJsonArray are embedded as-is.
The framework frees the object you return. TJSONRPCResponse.Destroy does
if FResult.IsObject then FResult.AsObject.Free. So:
// CORRECT โ allocate and return; the framework frees it
function TMyJSONRPCService.GetUser(aUserName: string): TPerson;
begin
Result := TPerson.Create;
Result.FirstName := aUserName;
end;
function TMyJSONRPCService.GetServerInfo: TJDOJsonObject;
begin
Result := TJDOJsonObject.Create;
Result.S['serverName'] := 'DMVCFramework JSON-RPC Server';
end;
There is no ToFree here โ ToFree<T> is a TMVCRenderer helper for controller actions, and the
published RPC class is a plain TObject that does not have it. Free your own intermediates with
try/finally, exactly as TMyObject.GetCustomers does in samples/jsonrpc/jsonrpcserver/MyObjectU.pas.
Do not return a cached or shared object (a singleton, a field, an object owned by a list) โ it will be freed
under you. Return a clone: the sample does Result := WithJSON.Clone as TJsonObject.
A procedure returns nothing; the response is HTTP 204 with no body (ยง5).
5. Requests vs notifications โ and no batch
| Request | Notification |
|---|
| JSON | has an id member | no id member |
| Delphi method | must be a function | must be a procedure |
| HTTP response | 200 + {"jsonrpc":"2.0","result":โฆ,"id":โฆ} | 204 No Content, empty body |
Both directions are enforced. Calling a procedure with an id raises "Cannot call a procedure using a
JSON-RPC request - use requests for functions and notifications for procedures"; calling a function
without an id raises the mirror message. Both come back as -32602 Invalid params.
id may be a string or an integer. An id present but of another type is rejected.
Batch requests are not supported. The payload is parsed as a single JSON object
(GetPayloadFromRequest โ StrToJSONObject); an array of calls is not dispatched. One call per HTTP
request. Do not tell the user otherwise.
6. Errors
Errors are returned with HTTP status 200, in the JSON body, per the JSON-RPC 2.0 spec. The framework
comments say so explicitly and set ResponseStatus(200) in every exception branch. Do not write a client
that keys off the HTTP status.
Raising an error from an RPC method
uses MVCFramework.JSONRPC;
raise EMVCJSONRPCError.Create(JSONRPC_USER_ERROR + 1, 'Division by zero');
raise EMVCJSONRPCError.Create(JSONRPC_USER_ERROR + 2, 'Not found', lExtraJsonObject); // "data" member
raise EMVCJSONRPCError.CreateFmt(JSONRPC_USER_ERROR + 3, 'Customer %d is locked', [ID]);
An object passed as Data is freed by the framework (TJSONRPCResponseError.Destroy). Do not free it.
Any other exception that escapes an RPC method is caught and returned as an error with code 0 and the
exception message (plus the class name in data), unless the endpoint has an exception handler (below).
Exception classes (MVCFramework.JSONRPC.pas)
| Class | Code it produces |
|---|
EMVCJSONRPCError | whatever you pass |
EMVCJSONRPCServerError | whatever you pass โ intended for the reserved server range |
EMVCJSONRPCParseError | -32700 |
EMVCJSONRPCInvalidRequest | -32600 |
EMVCJSONRPCMethodNotFound | -32601 |
EMVCJSONRPCInvalidParams | -32602 |
EMVCJSONRPCInternalError | -32603 |
All descend from EMVCJSONRPCErrorResponse, which exposes JSONRPCErrorCode and JSONRPCErrorData.
Client side: EMVCJSONRPCRemoteException (ErrCode, ErrMessage, Data) and its descendant
EMVCJSONRPCProtocolException. EMVCJSONRPCException and EMVCJSONRPCInvalidVersion are plain exceptions.
Reserved codes
JSONRPC_ERR_PARSE_ERROR = -32700;
JSONRPC_ERR_INVALID_REQUEST = -32600;
JSONRPC_ERR_METHOD_NOT_FOUND = -32601;
JSONRPC_ERR_INVALID_PARAMS = -32602;
JSONRPC_ERR_INTERNAL_ERROR = -32603;
JSONRPC_ERR_SERVER_ERROR_LOWERBOUND = -32099;
JSONRPC_ERR_SERVER_ERROR_UPPERBOUND = -32000;
JSONRPC_USER_ERROR = JSONRPC_ERR_SERVER_ERROR_LOWERBOUND; // -32099
Application errors go in -32099 .. -32000. Idiom used by the samples and the wizard:
JSONRPC_USER_ERROR + n.
Per-endpoint exception handler
The third argument of PublishObject maps arbitrary Delphi exceptions onto JSON-RPC error objects:
TMVCJSONRPCExceptionHandlerProc = reference to procedure(E: Exception; // MVCFramework.pas
WebContext: TWebContext;
var ErrorInfo: TMVCJSONRPCExceptionErrorInfo; // record: Code: Integer; Msg: string; Data: TValue
var ExceptionHandled: Boolean);
AEngine.PublishObject(
function: TObject
begin
Result := TMyJSONRPCService.Create;
end, '/jsonrpc',
procedure(Exc: Exception; WebContext: TWebContext;
var ErrorInfo: TMVCJSONRPCExceptionErrorInfo; var ExceptionHandled: Boolean)
begin
if Exc is EDivByZero then
begin
ErrorInfo.Code := 888;
ErrorInfo.Msg := 'Custom Message: ' + Exc.Message;
ErrorInfo.Data := 'You cannot divide by 0'; // TValue: string or a TObject
ExceptionHandled := True;
end
else
ExceptionHandled := False;
end);
An object in ErrorInfo.Data is freed by the framework in both the handled and unhandled paths.
7. Hooks
Declare any of these three on the published class. The names are matched by RTTI and are exact โ
OnBeforeRoutingHook, OnBeforeCallHook, OnAfterCallHook. The signature is fixed; a wrong one raises
EMVCJSONRPCException at call time rather than being ignored:
procedure OnBeforeRoutingHook(const Context: TWebContext; const JSON: TJDOJsonObject);
procedure OnBeforeCallHook(const Context: TWebContext; const JSONRequest: TJDOJsonObject);
procedure OnAfterCallHook(const Context: TWebContext; const JSONResponse: TJDOJsonObject);
Both parameters must be const, the method must be a procedure, and there must be exactly two of them.
| Hook | When | Notes |
|---|
OnBeforeRoutingHook | after the payload is parsed, before the method is resolved | receives the raw request JSON; the method name is re-read from it afterwards, so the hook can remap method |
OnBeforeCallHook | after parameters are bound, immediately before the invoke | receives the request JSON; raise EMVCJSONRPCError here to reject the call |
OnAfterCallHook | after the call, including on error | receives the response JSON โ nil for a notification; always test if Assigned(JSONResponse) |
These are the only place a PublishObject-published class sees the TWebContext. The instance is
per-request, so stashing Context in a field from OnBeforeCallHook is safe.
8. Authentication
The JSON-RPC endpoint is a controller, so the standard middleware applies unchanged โ add
TMVCJWTAuthenticationMiddleware or TMVCBasicAuthenticationMiddleware in ConfigureEngine exactly as
documented in dmvcframework, and use the dmvcframework-security skill for hardening.
What is different:
[MVCRequiresAuthentication] has nothing to attach to. With PublishObject the controller class is
the framework's own TMVCJSONRPCPublisher. Auth is decided by your IMVCAuthenticationHandler:
OnRequest(AContext, AControllerQualifiedClassName, AActionName, var AAuthenticationRequired) is called
with the controller class name and action, not your RPC class or method name. Gate on the URL
(AContext.Request.PathInfo) if you need finer granularity, or descend from TMVCJSONRPCController and
put the attribute on your own class.
- Authorization is all-or-nothing at the endpoint. The handler runs before the body is parsed, so it
cannot see which RPC method was requested. For per-method authorization, check inside
OnBeforeCallHook โ you have both Context.LoggedUser and JSONRequest.S['method'] there โ and raise
EMVCJSONRPCError to reject.
- The login endpoint of the JWT middleware is a separate URL segment; it is not an RPC method.
samples/jsonrpc/AuthenticationU.pas has a TAuthenticationSample implementing IMVCAuthenticationHandler
for this sample set.
9. Introspection endpoints
TMVCJSONRPCController publishes two extra actions relative to the segment:
| Route | What |
|---|
GET /jsonrpc/describe | text/plain listing of every invokable method with its Delphi declaration, its [MVCDoc] text, and whether it is POST-only or POST+GET |
GET /jsonrpc/proxy | generated client code โ ?language=delphi (default), ?content-type=โฆ |
/proxy requires a proxy generator to have been registered via
RegisterJSONRPCProxyGenerator(aLanguage, aClass); no generator ships in sources/, so out of the box this
route raises "No Proxy Generators have been registered". /describe always works. Both are public โ put
them behind auth or a middleware if the method list is sensitive.
10. The client โ IMVCJSONRPCExecutor
Calling a JSON-RPC endpoint from Delphi: read reference/client.md. It covers
TMVCJSONRPCExecutor.Create, ExecuteRequest / ExecuteNotification and their async twins, building an
IJSONRPCRequest with positional or named params, reading the response (Result, ResultAs, Error),
error handling, headers/TLS/tracing, and the client-side ownership rules.
Two things to know before you write a single line of client code:
TJSONRPCRequestParams owns and frees every object you hand it via Params.Add / AddByName.
- With the default
aRaiseExceptionOnError = True, an error response raises EMVCJSONRPCRemoteException โ
read .ErrCode / .ErrMessage / .Data from that, not from lResp.Error.
11. Worked example, end to end
JSONRPCServiceU.pas โ the published class
unit JSONRPCServiceU;
interface
uses
MVCFramework, MVCFramework.Commons, MVCFramework.JSONRPC, JsonDataObjects,
System.Generics.Collections, EntitiesU;
type
TMyJSONRPCService = class
public
[MVCDoc('Divides A by B. Raises a JSON-RPC error if B is zero.')]
function Divide(const A, B: Double): Double;
[MVCDoc('Returns a customer by id.')]
function GetCustomer(const ID: Integer): TCustomer;
// MaxRows may be omitted โ but only by a client sending NAMED params.
[MVCDoc('Searches customers. Callable with GET too.')]
[MVCJSONRPCAllowGET]
function SearchCustomers(const Query: string;
[MVCJSONRPCOptional] const MaxRows: Integer): TObjectList<TCustomer>;
// Notification: the client sends no "id"; the server answers 204 with no body.
[MVCDoc('Logs a message on the server.')]
procedure LogMessage(const AMessage: string; const ALevel: string);
procedure OnBeforeCallHook(const Context: TWebContext; const JSONRequest: TJDOJsonObject);
procedure OnAfterCallHook(const Context: TWebContext; const JSONResponse: TJDOJsonObject);
end;
implementation
uses
System.SysUtils, MVCFramework.Logger, MVCFramework.ActiveRecord;
function TMyJSONRPCService.Divide(const A, B: Double): Double;
begin
if B = 0 then
raise EMVCJSONRPCError.Create(JSONRPC_USER_ERROR + 1, 'Division by zero');
Result := A / B;
end;
function TMyJSONRPCService.GetCustomer(const ID: Integer): TCustomer;
begin
Result := TMVCActiveRecord.GetByPk<TCustomer>(ID); // framework frees the result
end;
function TMyJSONRPCService.SearchCustomers(const Query: string;
[MVCJSONRPCOptional] const MaxRows: Integer): TObjectList<TCustomer>;
begin
Result := TMVCActiveRecord.Where<TCustomer>('name like ?', ['%' + Query + '%']);
end;
procedure TMyJSONRPCService.LogMessage(const AMessage, ALevel: string);
begin
LogI(Format('[%s] %s', [ALevel, AMessage]));
end;
procedure TMyJSONRPCService.OnBeforeCallHook(const Context: TWebContext;
const JSONRequest: TJDOJsonObject);
begin
// Per-method authorization: raise EMVCJSONRPCError here to reject the call.
LogI('JSON-RPC call: ' + JSONRequest.S['method']);
end;
procedure TMyJSONRPCService.OnAfterCallHook(const Context: TWebContext;
const JSONResponse: TJDOJsonObject);
begin
if Assigned(JSONResponse) then // nil for notifications
LogI('JSON-RPC response ready for id: ' + JSONResponse.S['id']);
end;
end.
EngineConfigU.pas โ registration
uses
Controllers.HomeU, JSONRPCServiceU, MVCFramework.JSONRPC, MVCFramework.Middleware.CORS;
procedure ConfigureEngine(AEngine: TMVCEngine);
begin
// Controllers
AEngine.AddController(THomeController);
// Controllers - END
// Middleware
AEngine.AddMiddleware(TMVCCORSMiddleware.Create);
// Middleware - END
AEngine.PublishObject(
function: TObject
begin
Result := TMyJSONRPCService.Create;
end, '/jsonrpc');
end;
Client
uses MVCFramework.JSONRPC, MVCFramework.JSONRPC.Client;
var lExecutor: IMVCJSONRPCExecutor := TMVCJSONRPCExecutor.Create('http://localhost:8080');
// request โ function
var lReq := lExecutor.CreateRequest('getcustomer', 1);
lReq.Params.AddByName('ID', 42);
var lCustomer := TCustomer.Create;
try
lExecutor.ExecuteRequest('/jsonrpc', lReq).ResultAs(lCustomer);
Writeln(lCustomer.Name);
finally
lCustomer.Free;
end;
// notification โ procedure
var lNot := lExecutor.CreateNotification('logmessage');
lNot.Params.AddByName('AMessage', 'hello');
lNot.Params.AddByName('ALevel', 'INFO');
lExecutor.ExecuteNotification('/jsonrpc', lNot);
Testing an RPC endpoint over real HTTP: dmvcframework-testing.
12. Common mistakes
| Mistake | Reality |
|---|
published methods on the RPC class | Only public is invokable. published is mvPublished and is silently invisible |
AEngine.AddController(TMyJSONRPCService) | A plain class is not a controller. Use AEngine.PublishObject(<delegate>, '/jsonrpc') |
[MVCPath] / [MVCHTTPMethod] on RPC methods | There is no per-method routing. One URL, dispatch by method name |
[JsonRpcMethod], RegisterMethod, AddRpcHandler | None exist. Nothing marks a method as RPC โ visibility does |
A function called as a notification (or a procedure as a request) | Rejected with -32602. function = request, procedure = notification |
| Expecting HTTP 400/404/500 on an error | Errors always come back with HTTP 200 and an error member. 204 means a notification succeeded |
| Sending a batch (JSON array of calls) | Not supported. One call per HTTP request |
| Freeing an object parameter inside the method | The framework frees every object parameter after the call โ double free |
| Freeing the object you return | The framework frees the result too โ double free |
Result := ToFree(...) | ToFree is a TMVCRenderer method; a PublishObject-published class does not have it |
Returning a shared/cached object or a [MVCOwned] child | It gets freed. Return a clone |
Freeing an object after Params.Add/AddByName on the client | TJSONRPCRequestParams owns and frees it |
lResp.Error.Message | The property is ErrMessage |
var/out parameters | Raise EMVCJSONRPCInvalidParams. Only const and by-value |
Calling with GET without [MVCJSONRPCAllowGET] | -32600 Invalid Request, "Method callable with POST only" |
| Renaming a formal parameter | Named params match the Delphi parameter name โ it is part of the contract |
Context inside a PublishObject class | It is a plain TObject. Get TWebContext from a hook, or descend from TMVCJSONRPCController |
[MVCRequiresAuthentication] on the RPC class | Nothing reads it for TMVCJSONRPCPublisher. Auth comes from the middleware's IMVCAuthenticationHandler |
| Overloading an invokable method | Lookup is by name and returns the first match |
13. Key units
| Unit | Purpose |
|---|
MVCFramework.JSONRPC | TMVCJSONRPCController, TMVCJSONRPCPublisher, [MVCJSONRPCAllowGET], [MVCJSONRPCOptional], [MVCJSONRPCRestParams], TJSONRPCRequest, IJSONRPCRequest/Notification/Response, TJSONRPCRequestParams, TJSONRPCParamDataType, all EMVCJSONRPC* classes, the JSONRPC_* constants |
MVCFramework.JSONRPC.Client | IMVCJSONRPCExecutor, IMVCJSONRPCExecutorAsync, TMVCJSONRPCExecutor, TJSONRPCResponseHandlerProc, TJSONRPCErrorHandlerProc |
MVCFramework | TMVCEngine.PublishObject, TMVCObjectCreatorDelegate, TMVCJSONRPCExceptionHandlerProc, TMVCJSONRPCExceptionErrorInfo, TWebContext, [MVCInject], [MVCDoc], [MVCInheritable] |
JsonDataObjects | TJDOJsonObject, TJDOJsonArray (aliases of TJsonObject/TJsonArray) |
MVCFramework.Serializer.JsonDataObjects | TMVCJsonDataObjectsSerializer โ the serializer used for params and results |
MVCFramework.Serializer.Commons | TMVCSerializationType (stDefault, stProperties, stFields), [MVCNameCase], [MVCNameAs] |
MVCFramework.Server.Factory | TMVCServerFactory.CreateIndyDirect / CreateHttpSys / CreateWebBroker |
14. Reference samples
| Folder | Shows |
|---|
samples/jsonrpc/jsonrpcserver/ | PublishObject (three endpoints, one with an exception handler); MyObjectU.pas covers datasets, objects, enums, sets, records, arrays, hooks and error raising |
samples/jsonrpc/sync_client/ | IMVCJSONRPCExecutor, positional and named params, ResultAs, notifications, GET calls, tracing callbacks, HTTP headers |
samples/jsonrpc/async_client/ | IMVCJSONRPCExecutorAsync, ExecuteRequestAsync / ExecuteNotificationAsync |
samples/jsonrpc/AuthenticationU.pas | IMVCAuthenticationHandler for this sample set |
unittests/general/TestServer/TestServerControllerJSONRPCU.pas | TMVCJSONRPCController descendants, [MVCInheritable], records and complex types over the wire |
Related skills: dmvcframework (core: engine, middleware, entities, DI) ยท
dmvcframework-security (required for any endpoint a stranger can call) ยท
dmvcframework-testing (DUnitX integration tests).