Custom Error XPage: Ability for Browser to Load and Execute JS Script Link or Block After Being Loaded On Error

1.2k views Asked by At

Situation

I'm using a custom error XPage, based highly off of the XSnippet from Tony McGuckin. It works rather well but I would like for the browser to execute a client-side JavaScript block (or load and run a JS file from a given URL). If I navigate directly to the custom error XPage, it loads correctly, but given the nature of how it loads on redirect from a SSJS runtime error, it seems to load any attempts at loading a script block in the head tag, inside the body tag. I've attempted passing through a JS script tag in the body (shown in the code below), attempted using the xp:headTag inside xp:resources, and attempted via an xp:script tag in xp:resources.

Browser's Perspective

From the browser's perspective, after encountering a runtime error during an event that invokes SSJS during a partial refresh, the xhr being invoked returns with a 500 and sets the content into the body tag (screen shot).

error XPage loading

When viewing the response contents, the entire custom error XPage is there, including the <script type="text/javascript">console.log("hello world");<script>. This does not seem to trigger or put anything out to the JS console of the browser. What is visible via the JS console is some garbage from dojo complaining about getting back an XHR with response code of 500 (my dojoConfig is set to isDebug: true via xsp.client.script.dojo.djConfig in XSP Properties).

Question

Is there a way to get a client-side JS script tag to load and execute in the browser after an error 500 which occurs during the loading of a custom error XPage?


Here's the code for my Error page. To reproduce my results, invoke an SSJS action resulting in a runtime error (such as the ErrorOnClick XPage included in the OpenLog Logger for XPages project from Paul Withers) with a partial refresh event.

Error.xsp (set as the error page in XSP Properties)

<?xml version="1.0" encoding="UTF-8"?>
<xp:view
    xmlns:xp="http://www.ibm.com/xsp/core"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.ibm.com/xsp/core xsdxp://localhost/xsp~core.xsd"
    pageTitle="${javascript:database.getTitle() + ' | Error'}">
    <style
        type="text/css"><![CDATA[
        body {
            background-color: lightblue;
        }
        form {
            width: 1000px !important;
            margin: 0 auto !important;
            background-color: white !important;
            margin-top: 2rem !important;
            padding: 0.5rem !important;
            height: auto;
        }
        .xspTextLabel {
            font-weight: bold !important;
        }
  ]]></style>
    <img
        class="logo-simple"
        src="//placehold.it/124x32" />
    <xp:panel>
        <xp:br></xp:br>
        <xp:br></xp:br>
        <xp:label
            style="font-weight:bold;font-size:12pt"
            value="An Unexpected Error Has Occurred:">
        </xp:label>
        <xp:br></xp:br>
        <xp:br></xp:br>
        <xp:label
            value="Error:"></xp:label>
        <xp:br />
        <xp:text
            escape="false">
            <xp:this.value><![CDATA[#{javascript:if( !!requestScope.error ){
    var output = (requestScope.error.toString() || null)+"<br /><br />";
    if(requestScope.error instanceof com.ibm.xsp.exception.XSPExceptionInfo){
        var codeSnippet = requestScope.error.getErrorText(); 
        var control = requestScope.error.getErrorComponentId();
        var cause = requestScope.error.getCause();
        output += "In the control : " + control + "<br /><br />";
        if(cause instanceof com.ibm.jscript.InterpretException){
            var errorLine = cause.getErrorLine();
            var errorColumn = cause.getErrorCol();
            output += "At line " + errorLine;
            output += ", column " + errorColumn + " of:<br />";
        }else{
            output += "In the script:<br />";
        }
        if( @Contains(codeSnippet,"#{javascript:") ){
            var snipAr = codeSnippet.split("#{javascript:");
            var tmpSnip = snipAr[1];
            var nwSnip = tmpSnip.substring(0, tmpSnip.length - 1);
            output += "#{javascript:<br /><pre>"+nwSnip+"</pre>}"
        }else{
            output += codeSnippet;
        }
    }
    return output;
}else{
    return "";
}}]]></xp:this.value>
        </xp:text>
        <xp:br></xp:br>
        <xp:br></xp:br>
        <xp:label
            value="Stack Trace:"></xp:label>
        <xp:br />
        <xp:text
            escape="false"
            style="font-size:10pt">
            <xp:this.value><![CDATA[#{javascript:if( !!requestScope.error ){
    var stackTrace = "";
    var trace = (requestScope.error.getStackTrace() || null);
    if(trace != null){
        for(var i = 0; i < trace.length; i++){
            stackTrace += trace[i] + "<br/>";
        }
        return "<pre>"+stackTrace+"</pre>";
    }else{
        return "nothing";
    }
}else{
    return "";
}}]]></xp:this.value>
        </xp:text>
    </xp:panel>
    <script
        type="text/javscript">
        <![CDATA[console.log("Hello world...");]]>
    </script>
</xp:view>

For what it's worth: I didn't find anything explicitly on this subject via a search of Google or StackOverflow.


UPDATE 1: This was a case of either caffeine deprivation or just not seeing the forest through the trees. It helps to not use a CDATA block in your HTML code. The lazy developer in me tried copying and pasting between an xp:script block and the HTML <script> block, preserving it. Now for the public shaming of buying Marky beer in Atlanta.

UPDATE 2: Marky's beverage of choice may be in peril. While I seem to have had issues with copying a CDATA tag in, the issue remains. In my efforts to produce a simplified page with a button to error out (loosely based on the above mentioned XPage from the OpenLog Logger for XPages ErrorOnClick.xsp), I mistakenly took out a part of what was causing my issues in the first place, the partial refresh. When I do a full refresh, no issue, but when I do a partial, it doesn't load. I'm enclosing a sample page to trigger an error, with two buttons; one to induce a full, the other a partial. SO, with a full refresh, I get an alert of "hello world...", with the partial, no dice.

MakeSomeError.xsp

<?xml version="1.0" encoding="UTF-8"?>
<xp:view
    xmlns:xp="http://www.ibm.com/xsp/core">
    <xp:panel
        id="somePanel">
        <xp:button
            value="Failing Partial"
            id="button1">
            <xp:eventHandler
                event="onclick"
                submit="true"
                refreshMode="partial"
                refreshId="somePanel">
                <xp:this.action><![CDATA[#{javascript:var a:NotesDateTime = null;
viewScope.myStuff = a.toJavaDate().toDateString();}]]></xp:this.action>
            </xp:eventHandler>
        </xp:button>
        <xp:button
            value="Failing Full"
            id="button2">
            <xp:eventHandler
                event="onclick"
                submit="true"
                refreshMode="complete">
                <xp:this.action><![CDATA[#{javascript:var a:NotesDateTime = null;
viewScope.myStuff = a.toJavaDate().toDateString();}]]></xp:this.action>
            </xp:eventHandler>
        </xp:button>
        <xp:br />
        <xp:text
            value="#{viewScope.myStuff}" />
    </xp:panel>
</xp:view>

UPDATE 3: Okay. Sven's second answer has me very close, but for some reason I can't extrapolate just enough to get what I want to happen to occur. I'm including a GIF below of my results. The only thing different I would like to have happen is for my Error.xsp (custom error XPage) to continue loading after I encounter the error (it seems like I'll need to change the beforeRenderResponse block to an afterRenderResponse script perchance?). I want to append the script, not replace the Error.xsp loading. Basically, I'm trying to run a script after the error XPage is loaded (there's a helper JS file I'm trying to load into my custom error XPage, CSS is loading fine, just not the JS lib). I would love to:

  1. get this working
  2. share what it is (it's kind of cool, if I do say so myself)

results of Sven's second answer

4

There are 4 answers

2
Sven Hasselbach On BEST ANSWER

OK, last try. A very interesting one. On your Error.xsp, add the following image:

<xp:text
    escape="true"
    id="executeOnAjax"
    tagName="img">
    <xp:this.attrs>
        <xp:attr
            name="src"
            value="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==">
    </xp:attr>
        <xp:attr
            name="onload"
            value="alert('Hello World!');this.parentNode.removeChild(this);">
    </xp:attr>
    </xp:this.attrs>
    <xp:this.rendered>
        <![CDATA[#{javascript:
            var ex = facesContext.getExternalContext();
            var pMap = com.ibm.xsp.util.TypedUtil().getRequestParameterMap(ex);
            var refreshId = pMap.get("$$ajaxid");
            refreshId?true:false;}]]>
    </xp:this.rendered>
</xp:text>

if this doesn't fit your requirements, I don't understand what you are trying to accomplish.

1
Sven Hasselbach On

Marky has another good idea: Hijack the response.

This could look like this:

<script>
    if( !dojo._xhr )
        dojo._xhr = dojo.xhr;

        var myHandler = function(){
            var xhrObj = arguments[1].xhr;
            var response = xhrObj.response;
            var header = xhrObj.getResponseHeader('X-XspRefreshId');
            if( header == "@error" ){
                eval( response );
            }else{
                arguments[1]["error"]( arguments[0], arguments[1]);
            }
        }

    dojo.xhr = function(){        
        try{
            var args = arguments[1];   
            args["failOk"] = true;
            args["error"] = myHandler;
            arguments[1] = args;
        }catch(e){}

        dojo._xhr( arguments[0], arguments[1], arguments[2] );
    }
</script>

The beforeRenderResponse event has to be modified like this:

<xp:this.beforeRenderResponse>
    <![CDATA[#{javascript:    
    var ex = facesContext.getExternalContext();
    var pMap = com.ibm.xsp.util.TypedUtil().getRequestParameterMap(ex);
    var refreshId = pMap.get("$$ajaxid");

    if( refreshId ){
        var resp:com.ibm.xsp.webapp.XspHttpServletResponse = facesContext.getExternalContext().getResponse();
        var writer:java.io.PrintWriter = resp.getWriter();
        writer.write( "alert('Hello World!' );\n" );
        resp.setHeader('X-XspRefreshId', '@error' );
        facesContext.responseComplete();
    }
}]]>
</xp:this.beforeRenderResponse>
1
Sven Hasselbach On

The reason for this is behaviour is that this is a security feature. Browsers don't execute <script> blocks if they where loaded via Ajax. But there is a workaround:

First you have to add a div for replacement to your calling XPage:

<div id="errRefresh" />

This is just a placeholder for the partial refresh, otherwise it will fail.

Now, you have to modify your error page to handle the partial refreshs. To do this, you have to detect if it is a refresh or not, but you cannot use the build-in functionality (it is nulled in an error page). So you have to do this by your own:

var ex = facesContext.getExternalContext();
var pMap = com.ibm.xsp.util.TypedUtil().getRequestParameterMap(ex);
var refreshId = pMap.get("$$ajaxid");

Now you must set the response status code to 200, otherwise the error method from the event is called:

var resp:com.ibm.xsp.webapp.XspHttpServletResponse = facesContext.getExternalContext().getResponse();
resp.setStatus(200);

Then, you can add your CSJS block which must look like this:

<!-- XSP_UPDATE_SCRIPT_START -->
<script>
   alert('Hello World!');
</script>
<!-- XSP_UPDATE_SCRIPT_END -->

When the parial refresh is processed, the refreshed DOM element is replaced, that's why we have to resend the HTML markup with the response, and overwrite the X-XspRefreshId to force replacement of our error element instead:

resp.setHeader('X-XspRefreshId', 'errRefresh' );

Last but not least, we have to skip the JSF lifecycle:

facesContext.responseComplete();

That's it.

Here is the complete code for the beforeRenderResponse event of the error page:

<xp:this.beforeRenderResponse>
    <![CDATA[#{javascript:    
    var ex = facesContext.getExternalContext();
    var pMap = com.ibm.xsp.util.TypedUtil().getRequestParameterMap(ex);
    var refreshId = pMap.get("$$ajaxid");

    if( refreshId ){
        var resp:com.ibm.xsp.webapp.XspHttpServletResponse = ex.getResponse();
        var writer:java.io.PrintWriter = resp.getWriter();
        writer.write( "<!-- XSP_UPDATE_SCRIPT_START -->\n" );
        writer.write( "<script>\n");
        writer.write( "alert('Hello World!' );\n" );
        writer.write( "</script>\n");
        writer.write( "<!-- XSP_UPDATE_SCRIPT_END -->\n" );
        writer.write( "<div id=\"errRefresh\" />\n");
        resp.setStatus(200);
        resp.setHeader('X-XspRefreshId', 'errRefresh' );
        facesContext.responseComplete();
    }
}]]>
</xp:this.beforeRenderResponse>

Keep in mind that this might result in security issues.

3
MarkyRoden On

Don't use CDATA in your script tags.

<script>
    alert('hi Marky!');
</script>

Works for me.