Question
Eduard Lebedyuk · May 23, 2016

REST and IO redirection

Problem: I have a REST broker, and if I hit a code block, which does IO redirection, the REST reply becomes broken in one of the following ways:

  • Binary output
  • No output
  • First 4096 characters of the reply are missing

Consider the following REST broker:

Class A.REST Extends %CSP.REST
{

XData UrlMap
{
<Routes>
<Route Url="/Test/:redirect" Method="GET" Call="Test"/>
 </Routes>
}

ClassMethod Test(Redirect As %Boolean = {$$$YES}) As %Status
{
    Set str = $TR($J("",4098)," ","1") // Get a string with the length of 4098 symbols
    
    Do:Redirect ..OutputToStr($Classname(), "UnrelatedAction")
    Write str
    Return $$$OK
}

ClassMethod UnrelatedAction()
{
    Set a=1
}

/// Executes actions and returns device output <br>
/// pObj - OREF or class<br>
/// pMethod - instance or class method to execute respectively<br>
/// pArgs - additional arguments
ClassMethod OutputToStr(pObj, pMethod, pArgs...) As %String [ ProcedureBlock = 0 ]
{
    try {
        set str=""

        //Redirect IO to the current routine - makes use of the labels defined below
        use $io::("^"_$ZNAME)

        //Enable redirection
        do ##class(%Device).ReDirectIO(1)

            if $isobject(pObj) {
                do $Method(pObj,pMethod,pArgs...)
            } elseif $$$comClassDefined(pObj) {
                do $ClassMethod(pObj,pMethod,pArgs...)
            }

        //Disable redirection
        do ##class(%Device).ReDirectIO(0)
    } catch ex {
        
        if ##class(%Device).ReDirectIO() {
            do ##class(%Device).ReDirectIO(0)
        }
        set str = ""
    }

    quit str

    //Labels that allow for IO redirection
    //Read Character - we don't care about reading
rchr(c)      quit
    //Read a string - we don't care about reading
rstr(sz,to)  quit
    //Write a character - call the output label
wchr(s)      do output($char(s))  quit
    //Write a form feed - call the output label
wff()        do output($char(12))  quit
    //Write a newline - call the output label
wnl()        do output($char(13,10))  quit
    //Write a string - call the output label
wstr(s)      do output(s)  quit
    //Write a tab - call the output label
wtab(s)      do output($char(9))  quit
    //Output label - this is where you would handle what you actually want to do.
    //  in our case, we want to write to str
output(s)    set str=str_s   quit
}

}

After I create a webapplication (name /test) with this broker, I expect that these two requests:

http://localhost:57772/test/Test/0
http://localhost:57772/test/Test/1

Would return the same result (namely, a string containing 4098  symbols "1").

But in reality the second call would return only 2 symbols "1". The only difference between calls, is that method OutputToStr, which contains IO redirection gets called.

How do I modify OutputToStr method, so that it does not affect REST output?

$zv  Cache for Windows (x86-64) 2016.1 (Build 656U) Fri Mar 11 2016 17:42:42 EST

Download GitHub Gist with code.

40
1 1 3 809
Log in or sign up to continue

The problem is that REST uses IO redirection itself, and OutputToStr changes the mnemonic routine but doesn't change it back at the end.

For a great example of the general safe approach to cleaning up after IO redirection (restoring to the previous state of everything), see %WriteJSONStreamFromObject in %ZEN.Auxiliary.jsonProvider.

Here's a simple approach that works for me, in this case:

set tOldIORedirected = ##class(%Device).ReDirectIO()
set tOldMnemonic = ##class(%Device).GetMnemonicRoutine()
set tOldIO = $io
try {
	set str=""

	//Redirect IO to the current routine - makes use of the labels defined below
	use $io::("^"_$ZNAME)

	//Enable redirection
	do ##class(%Device).ReDirectIO(1)

	if $isobject(pObj) {
		do $Method(pObj,pMethod,pArgs...)
	} elseif $$$comClassDefined(pObj) {
		do $ClassMethod(pObj,pMethod,pArgs...)
	}
} catch ex {
	set str = ""
}

//Return to original redirection/mnemonic routine settings
if (tOldMnemonic '= "") {
	use tOldIO::("^"_tOldMnemonic)
} else {
	use tOldIO
}
do ##class(%Device).ReDirectIO(tOldIORedirected)

quit str

It would be cool if something like this could work instead:

new $io
try {
	set str=""

	//Redirect IO to the current routine - makes use of the labels defined below
	use $io::("^"_$ZNAME)

	//Enable redirection
	do ##class(%Device).ReDirectIO(1)

	if $isobject(pObj) {
		do $Method(pObj,pMethod,pArgs...)
	} elseif $$$comClassDefined(pObj) {
		do $ClassMethod(pObj,pMethod,pArgs...)
	}
} catch ex {
	set str = ""
}

quit str

But $io can't be new'd.

One important note on I/O redirection, from the documentation:

When a process performs I/O redirection, this redirection is performed using the user’s login $ROLES value, not the current $ROLES value.

In other words, if the I/O redirection is happening in a CSP application or privileged routine application context, in which matching or application roles are added that grant permissions on resources protecting assets the I/O functions need, you might get an unexpected <PROTECT> error, with roles seeming to disappear in wstr()/etc.

For the simple case of outputting to a string (as in this example) this is no big deal, but there may be issues with some types of streams, for example, if the stream implementation tries to set/kill globals. (This caught me by surprise the other day.)