How to VBA catch request timeout error?
There are several complications here.
MSXML2.ServerXMLHTTP
does not expose COM-usable events. Therefore it is not easily possible to instantiate an object usingWithEvents
and attach to itsOnReadyStateChange
event.
The event is there, but the standard VBA way to handle it does not work.- The module that could handle the event cannot be created using the VBA IDE.
- You need to call
waitForResponse()
when you use asynchronous requests (additionally to callingsetTimeouts()
!) - There is no
timeout
event. Timeouts are thrown as an error.
To resolve issue #1:
Usually a VBA class module (also applies to user forms or worksheet modules) allows you to do this:
Private WithEvents m_xhr As MSXML2.ServerXMLHTTP
so you can define an event handler like this:
Private Sub m_xhr_OnReadyStateChange() ' ...End Sub
Not so with MSXML2.ServerXMLHTTP
. Doing this will result in a Microsoft Visual Basic Compile Error: "Object does not source automation events".
Apparently the event is not exported for COM use. There is a way around this.
The signature for onreadystatechange
reads
Property onreadystatechange As Object
So you can assign an object. We could create a class module with an onreadystatechange
method and assign like this:
m_xhr.onreadystatechange = eventHandlingObject
However, this does not work. onreadystatechange
expects an object and whenever the event fires, the object itself is called, not the method we've defined. (For the ServerXMLHTTP
instance there is no way of knowing which method of the user-defined eventHandlingObject
we intend to use as the event handler).
We need a callable object, i.e. an object with a default method (every COM object can have exactly one).
(For example: Collection
objects are callable, you can say myCollection("foo")
which is a shorthand for myCollection.Item("foo")
.)
To resolve issue #2:
We need a class module with a default property. Unfortunately these can't be created using the VBA IDE, but you can create them using a text editor.
- prepare the class module that contains an
onreadystatechange
function in the VBA IDE - export it to a
.cls
file via right click - open that in a text editor and add the following line beneath the
onreadystatechange
signature:Attribute OnReadyStateChange.VB_UserMemId = 0
- remove the original class module and and re-import it from file.
This will mark the modified method as Default
. You can see a little blue dot in the Object Browser (F2), which marks the default method:
So every time the object is called, actually the OnReadyStateChange
method is called.
To resolve issue #3:
Simply call waitForResponse()
after send()
.
m_xhr.Sendm_xhr.waitForResponse timeout
In case of a timeout: If you did not call this method, the request simply never returns. If you did, an error is thrown after timeout
milliseconds.
To resolve issue #4:
We need to use an On Error
handler that catches the timeout error and transforms it into an event, for convenience.
Putting it all together
Here is a VB class module I wrote that wraps and handles an MSXML2.ServerXMLHTTP
object. Save it as AjaxRequest.cls
and import it into your project:
VERSION 1.0 CLASSBEGIN MultiUse = -1 'TrueENDAttribute VB_Name = "AjaxRequest"Attribute VB_GlobalNameSpace = FalseAttribute VB_Creatable = FalseAttribute VB_PredeclaredId = FalseAttribute VB_Exposed = FalseOption ExplicitPrivate m_xhr As MSXML2.ServerXMLHTTPAttribute m_xhr.VB_VarHelpID = -1Private m_isRunning As Boolean' default timeouts. TIMEOUT_RECEIVE can be overridden in requestPrivate Const TIMEOUT_RESOLVE As Long = 1000Private Const TIMEOUT_CONNECT As Long = 1000Private Const TIMEOUT_SEND As Long = 10000Private Const TIMEOUT_RECEIVE As Long = 30000Public Event Started()Public Event Stopped()Public Event Success(data As String, serverStatus As String)Public Event Error(data As String, serverStatus As String, xhr As MSXML2.ServerXMLHTTP)Public Event TimedOut(message As String)Private Enum ReadyState XHR_UNINITIALIZED = 0 XHR_LOADING = 1 XHR_LOADED = 2 XHR_INTERACTIVE = 3 XHR_COMPLETED = 4End EnumPublic Sub Class_Terminate() Me.CancelEnd SubPublic Property Get IsRunning() As Boolean IsRunning = m_isRunningEnd PropertyPublic Sub Cancel() If m_isRunning Then m_xhr.abort m_isRunning = False RaiseEvent Stopped End If Set m_xhr = NothingEnd SubPublic Sub HttpGet(url As String, Optional timeout As Long = TIMEOUT_RECEIVE) Send "GET", url, vbNullString, timeoutEnd SubPublic Sub HttpPost(url As String, data As String, Optional timeout As Long = TIMEOUT_RECEIVE) Send "POST", url, data, timeoutEnd SubPrivate Sub Send(method As String, url As String, data As String, Optional timeout As Long) On Error GoTo HTTP_error If m_isRunning Then Me.Cancel End If RaiseEvent Started Set m_xhr = New MSXML2.ServerXMLHTTP60 m_xhr.OnReadyStateChange = Me m_xhr.setTimeouts TIMEOUT_RESOLVE, TIMEOUT_CONNECT, TIMEOUT_SEND, timeout m_isRunning = True m_xhr.Open method, url, True m_xhr.Send data m_xhr.waitForResponse timeout Exit SubHTTP_error: If Err.Number = &H80072EE2 Then Err.Clear Me.Cancel RaiseEvent TimedOut("Request timed out after " & timeout & "ms.") Resume Next Else Err.Raise Err.Number, Err.Source, Err.Description, Err.HelpFile, Err.HelpContext End IfEnd Sub' Note: the default method must be public or it won't be recognizedPublic Sub OnReadyStateChange()Attribute OnReadyStateChange.VB_UserMemId = 0 If m_xhr.ReadyState = ReadyState.XHR_COMPLETED Then m_isRunning = False RaiseEvent Stopped ' TODO implement 301/302 redirect support If m_xhr.Status >= 200 And m_xhr.Status < 300 Then RaiseEvent Success(m_xhr.responseText, m_xhr.Status) Else RaiseEvent Error(m_xhr.responseText, m_xhr.Status, m_xhr) End If End IfEnd Sub
Note the line m_xhr.OnReadyStateChange = Me
, which assigns the AjaxRequest instance itself as the event handler, as made possible by marking OnReadyStateChange()
as the default method.
Be aware that if you make changes to OnReadyStateChange()
you need to go through the export/modify/re-import routine again since the VBA IDE does not save the "default method" attribute.
The class exposes the following interface
- Methods:
HttpGet(url As String, [timeout As Long])
HttpPost(url As String, data As String, [timeout As Long])
Cancel()
- Properties
IsRunning As Boolean
- Events
Started()
Stopped()
Success(data As String, serverStatus As String)
Error(data As String, serverStatus As String, xhr As MSXML2.ServerXMLHTTP)
TimedOut(message As String)
Use it in another class module, for example in a user form, with WithEvents
:
Option ExplicitPrivate WithEvents ajax As AjaxRequestPrivate Sub UserForm_Initialize() Set ajax = New AjaxRequestEnd SubPrivate Sub CommandButton1_Click() Me.TextBox2.Value = "" If ajax.IsRunning Then ajax.Cancel Else ajax.HttpGet Me.TextBox1.Value, 1000 End IfEnd SubPrivate Sub ajax_Started() Me.Label1.Caption = "Running" & Chr(133) Me.CommandButton1.Caption = "Cancel"End SubPrivate Sub ajax_Stopped() Me.Label1.Caption = "Done." Me.CommandButton1.Caption = "Send Request"End SubPrivate Sub ajax_TimedOut(message As String) Me.Label1.Caption = messageEnd SubPrivate Sub ajax_Success(data As String, serverStatus As String) Me.TextBox2.Value = serverStatus & vbNewLine & dataEnd SubPrivate Sub ajax_Error(data As String, serverStatus As String, xhr As MSXML2.ServerXMLHTTP) Me.TextBox2.Value = serverStatusEnd Sub
Make enhancements as you see fit. The AjaxRequest
class was merely a byproduct of answering this question.