Written by

Question Yone Moreno · Mar 5, 2021

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:

With headers:

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:

With the headers:

Besides if we check the trace we see:

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

Marc Mundt · Mar 5, 2021

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"/>
0
Yone Moreno  Mar 8, 2021 to Marc Mundt

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:

And the headers are:

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:

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

0
Yone Moreno  Mar 8, 2021 to Marc Mundt

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:

GET response:

In POSTMAN:

However,

Our custom method: consultarImagen which is a POST

Shows nothing in POSTMAN

Why?

POST request:

POST response:

In POSTMAN:

Could you help us?

Thanks for your replies

0
Marc Mundt  Mar 8, 2021 to Yone Moreno

Yone, try removing this line and test if it works:

Set pOutput=##class(%GlobalBinaryStream).%New()
0
Yone Moreno  Mar 8, 2021 to Marc Mundt

Thanks Marc Mundt for your help

You are right

When we removed that line, the response is shown in POSTMAN:

Thanks four your help Marc

How did you know we should remove that line?

0
Marc Mundt  Mar 8, 2021 to Yone Moreno

I noticed that retrievePerson doesn't instantiate pOutput.

0
Yone Moreno  Mar 9, 2021 to Marc Mundt

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

0
Eduard Lebedyuk · Mar 8, 2021

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.

0
Yone Moreno  Mar 9, 2021 to Eduard Lebedyuk

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

0
Eduard Lebedyuk  Mar 9, 2021 to Yone Moreno

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.

0
Utsavi Gajjar  Apr 8, 2021 to Eduard Lebedyuk

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

0