Creating a RESTFul Service, why custom method does not return the expected JSON?
FIrst of all thank you for your time in reading this question and writing a response,
We would need some help,
-> Our objective is to control which method is being used in the service: GET POST PUT
We have tried to understand the example REST Service: Demo.REST.DirectoryService
After that we have tried to create our own custom rest service,
please take a few minutes to examine the following code:
Code
We are able to use "retrievePerson" succesfully
We send:
http://localhost:19622/aplicaciones/scs/test/miscs/employee/name/Q*/sal…
We observe:
.png)
With headers:
.png)
Here comes the challenge:
When we issue:
http://localhost:19622/aplicaciones/scs/test/miscs/consultarImagen
With the following body:
{
"idApp": "miHistoria",
"usuario": "11473564",
"numExpediente": "11473564"
}
It replies nothing:
.png)
With the headers:
.png)
Besides if we check the trace we see:
.png)
.png)
.png)
Why we do not see the reply in POSTMAN?
Why headers are "text/html" and not "application/json"?
Could you help us?
We have read:
https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls…
https://docs.intersystems.com/latest/csp/docbook/Doc.View.cls?KEY=GREST…
Could you point us to some code, examples or documentation?
Thank you for your replies
Comments
If you are sending a body in your request to the REST service then it needs to be a POST, not a GET.
<Route Url="/consultarImagen" Method="GET" Call="consultarImagen"/>
Thanks Marc Mundt for your reply
Yes you are right, we should use POST
We have changed it:
<Route Url="/consultarImagen" Method="POST" Call="consultarImagen"/>However we do not see the response in POSTMAN:
.png)
And the headers are:
.png)
Content-Type: text/html
Content-Length: 0
CACHE-CONTROL: no-cache
PRAGAM: no-cache
We send the POST to the following URL:
http://localhost:19622/aplicaciones/scs/test/miscs/consultarImagen
Besides we observe the response message being converted from Ensemble object to JSON in the service:
.png)
.png)
We do see the Response Message from the Operation to the Service
Why we do not see the JSON being replied from the Service in POSTMAN?
How could we debug this behaviour?
Thanks for your replies
Thanks Marc Mundt for your attention, and your helpful reply
Our current code is:
Class Servicios.REST.miSCS.Pruebas Extends EnsLib.REST.Service
{
Parameter ADAPTER = "EnsLib.HTTP.InboundAdapter";
Parameter EnsServicePrefix = "/aplicaciones/scs/test/miscs";
XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
<Route Url="/:personType/:keyfield/:keyval/:getfield" Method="GET" Call="retrievePerson"/>
<Route Url="/consultarImagen" Method="POST" Call="consultarImagen"/>
</Routes>
}
/// Retrieve
Method retrievePerson(pInput As %Library.AbstractStream, Output pOutput As %Stream.Object, pPersonType As %String, pKeyField As %String, pKeyVal As %String, pGetField As %String = "") As %Status
{
Set tType=$ZConvert(pPersonType,"L") Quit:$Case(tType,"employee":0, "person":0, :1) $$$ERROR($$$EnsErrGeneral,"Directory type "_..#EnsServicePrefix_"/"_tType_"/ not supported.")
Set $E(tType)=$ZConvert($E(tType),"U")
Set tKeyIn=pKeyField, tKey=$ZConvert(tKeyIn,"L") Quit:$Case(tKey,"name":0, "ssn":0, :1) $$$ERROR($$$EnsErrGeneral,"Directory key "_..#EnsServicePrefix_"/"_tType_"/"_tKey_" not supported.")
Set tKeyVal=$Replace($ZConvert(pKeyVal,"I","URL"),"'","''")
Set tField=pGetField Set:""=tField tField="*" Quit:tField["," $$$ERROR($$$EnsErrGeneral,"Commas not allowed in selection field; found: .../"_tField)
Set tNS=$Namespace
Set tKeyWild=$Translate(pKeyVal,"*?","%_")
Do:tKeyWild'=pKeyVal pOutput.Write("[")
ZNSpace "SAMPLES"
try {
Set tSel=$S("*"=tField:"ID",1:tField)
Set tSQL="SELECT "_tSel_$S("*"=tField||(tKey=tSel):"", 1:","_tKey)_$Case("ID",tKey:"",tSel:"",:",ID")_" FROM Sample."_tType_" WHERE "_tKey_" LIKE '"_tKeyWild_"'"
//$$$LOGINFO("tSQL: "_tSQL)
Set tRS=##class(%ResultSet).%New()
Set tSC=tRS.Prepare(tSQL) Quit:$$$ISERR(tSC)
Set tSC=tRS.Execute() Quit:$$$ISERR(tSC)
Set tFirst=1
Set tOut=##class(%IO.StringStream).%New()
While tRS.Next(.tSC) && $$$ISOK(tSC) {
#; first normalize the case of the key and sel property names
If tFirst {
Set k="" For { Set k=$O(tRS.Data(k)) Quit:""=k
If $ZConvert(k,"L")=$Zconvert(tSel,"L") Set tSelN=k
If $ZConvert(k,"L")=$Zconvert(tKey,"L") Set tKeyN=k
}
}
If $Case(tSelN, "Company":1, "Notes":1, "Home":1, "Office":1, :0) {
Set tVal=tRS.Data("ID")
Set tObj=$classmethod("Sample."_tType,"%OpenId",tVal,,.tSC) Quit:$$$ISERR(tSC)
Set tVal=$property(tObj,tSelN)
Set tSelX = $Case(tSelN, "Home":"Addr", "Office":"Addr", :tSelN)
Set tVal=$Case(tSelX, "Company":tVal.Name, "Notes":tVal.Read(), "Addr":tVal.Street_", "_tVal.City_" "_tVal.State_" "_tVal.Zip, :tVal)
} Else {
Set tVal=tRS.Data(tSelN)
}
If "*"=tField {
Set tObj=$classmethod("Sample."_tType,"%OpenId",tVal,,.tSC) Quit:$$$ISERR(tSC)
Set tProxyObj=..buildProxyObj(tObj)
Do tOut.Write($S(tFirst:"",1:","))
Set tSC=..ObjectToJSONStream(tProxyObj,.tOut)
} Else {
Set:tKeyN'=tSelN tKeyFound=tRS.Data(tKeyN)
Do tOut.Write($S(tFirst:"",1:",")_"{"_$S(tKeyN=tSelN:"",1:""""_tKeyIn_""":"""_tKeyFound_""", ")_""""_tSel_""":"""_tVal_"""}"_$C(13,10))
}
Set tFirst=0
ZNSpace tNS
Do tOut.Rewind() Set tSC1=pOutput.Write(tOut.Read()) Do tOut.Clear() Set:$$$ISOK(tSC) tSC=tSC1 Quit:$$$ISERR(tSC)
ZNSpace "SAMPLES"
} Quit:$$$ISERR(tSC)
Do:tKeyWild'=tKeyVal pOutput.Write("]"_$C(13,10))
} catch {
Kill tRS
ZNSpace tNS
Set tSC=$$$SystemError
}
Kill tRS
ZNSpace tNS
$$$LOGINFO("tSQL: "_tSQL)
Do:$$$ISOK(tSC) pOutput.SetAttribute("Content-Type","application/json")
while (pOutput.AtEnd = 0){
set respuestaFinal = pOutput.Read()
}
do pOutput.Rewind()
$$$LOGINFO("respuestaFinal: "_respuestaFinal)
Quit tSC
}
/// Normalize the Person or Employee info by copying its properties to a proxy object in a selective way
ClassMethod buildProxyObj(pObj As %Persistent) [ Internal ]
{
Set tProxy = ##class(%ZEN.proxyObject).%New()
Set tProxy.ID=pObj.%Id()
Set tProxy.Name=pObj.Name
Set tProxy.Age=pObj.Age
Set tProxy.DOB=$ZDateTime(pObj.DOB,3)
Set tProxy.SSN=pObj.SSN
Set tProxy.FavoriteColors=pObj.FavoriteColors
Set tProxy.Spouse=pObj.Spouse.Name
Set tProxy.Home=..buildProxyAddr(pObj.Home)
Set tProxy.Office=..buildProxyAddr(pObj.Office)
If pObj.%IsA("Sample.Employee") {
Set tProxy.Company=pObj.Company.Name
Set tProxy.Notes=$S($IsObject(pObj.Notes):pObj.Notes.Read(),1:"")
}
Quit tProxy
}
ClassMethod buildProxyAddr(pObj As %SerialObject) [ Internal ]
{
Set tProxy = ##class(%ZEN.proxyObject).%New()
Set tProxy.Street=pObj.Street
Set tProxy.City=pObj.City
Set tProxy.State=pObj.State
Set tProxy.Zip=pObj.Zip
Quit tProxy
}
/// Control the type and content of error returned to the REST caller
ClassMethod OnErrorStream(pStatus As %Status)
{
Set tStream = ##class(%GlobalBinaryStream).%New() $$$ASSERT($IsObject(tStream))
Do tStream.Write($ZConvert($$$StatusDisplayString(pStatus)_$C(13,10),"O","UTF8"))
Set tStream.Attributes("Content-Type")=" text/plain; charset=""UTF-8"""
Set tStream.Attributes("ResponseCode")="500 Internal Server Error"
Quit tStream
}
/// Obtener la imagen guardada en TSI 📥📥📥📥
Method consultarImagen(pInput As %Library.AbstractStream, Output pOutput As %Stream.Object) As %Status
{
Set pOutput=##class(%GlobalBinaryStream).%New()
set claseAux = ##class(%ZEN.Auxiliary.jsonProvider).%New()
//Convertir el body del JSON a objeto de Ensemble
set body = pInput.Read()
$$$LOGINFO("miSCS: body: "_body)
do pInput.Rewind()
//El mensaje esta en el body
set tSC= claseAux.%ConvertJSONToObject(.body,"Mensajes.Request.miSCS.ConsultarImagen",.objetoEntrada,1)
//Enviamos al Proceso
set tSC = ..SendRequestSync("miSCS",objetoEntrada,.objetoSalida)
//Convertimos el OBJETO devuelto por el Proceso en JSON
//set tSC = claseAux.%WriteJSONStreamFromObject(.pOutput,.objetoSalida,,,,"aloqtuw")
//Esta linea hace esta en el metodo de ejemplo "retrievePerson" y convierte el mensaje response en JSON
Set tSC=..ObjectToJSONStream(objetoSalida,.pOutput)
while (pOutput.AtEnd = 0){
set respuesta = pOutput.Read()
}
do pOutput.Rewind()
$$$LOGINFO("respuesta: "_respuesta)
$$$LOGINFO("tSC: "_tSC)
$$$LOGINFO("$$$ISOK(tSC): "_$$$ISOK(tSC))
//Enviamos el JSON con cabeceras
Do:$$$ISOK(tSC) pOutput.SetAttribute("Content-Type","application/json")
do pOutput.SetAttribute("Access-Control-Allow-Origin","*")
do pOutput.SetAttribute("Access-Control-Allow-Credentials","true")
do pOutput.SetAttribute("Access-Control-Allow-Methods","GET")
do pOutput.SetAttribute("Access-Control-Allow-Headers","request,Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers")
while (pOutput.AtEnd = 0){
set respuestaFinal = pOutput.Read()
}
do pOutput.Rewind()
$$$LOGINFO("respuestaFinal: "_respuestaFinal)
Quit tSC
}
}When we dig deeper using Whireshark:
First, we find that the method in the example that uses GET, the retrievePerson, does answers with a JSON to POSTMAN, so it works:
GET request:
.png)
GET response:
.png)
In POSTMAN:
.png)
However,
Our custom method: consultarImagen which is a POST
Shows nothing in POSTMAN
Why?
POST request:
.png)
POST response:
.png)
In POSTMAN:
.png)
Could you help us?
Thanks for your replies
Yone, try removing this line and test if it works:
Set pOutput=##class(%GlobalBinaryStream).%New()Thanks Marc Mundt for your help
You are right
When we removed that line, the response is shown in POSTMAN:
.png)
Thanks four your help Marc
How did you know we should remove that line?
I noticed that retrievePerson doesn't instantiate pOutput.
We are grateful Marc for your help,
Thanks for explaining how did you find the cause which was preventing to get the response in POSTMAN
I would recommend using plain %CSP.REST and calling BP/BO from it. Here's how. You'll need to replace async call with a sync one to get response back, but the rest of the logic would be the same.
Thanks Eduard for sharing this interesting piece of advice
Why would be better or recommended to use %CSP.REST directly, instead of EnsLib.REST.Service?
Are there any improvements if we use %CSP.REST?
Thanks for your reply
Why would be better or recommended to use %CSP.REST directly, instead of EnsLib.REST.Service?
To start with using %CSP.REST is the recommended way to use REST with productions.
Are there any improvements if we use %CSP.REST?
It's easier to develop, there are more examples available, you can have a combined production/non-production REST services easily.
Hi Eduard,
I am looking for some examples of REST API served through production. Would you be able to point me to some, please?
I am trying to implement a business service, process and operation that serves bundle of apis related to a domain and resource e.g Patient , Patient alerts, patient status . These will be 3 api urls served by single business service, process and operation.
I am also looking for best practice to handle various http responses in BP and BO.
Would greatly appreciate if you can direct me to relevant examples.
Thank you,
Utsavi
Check this article.