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.

  • + 2
  • 0
  • 533
  • 2
  • 1

Answers

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.)