Repository navigation
Expand file tree
/
Copy pathclsHttpResponse.cls
More file actions
184 lines (166 loc) · 6.13 KB
/
Copy pathclsHttpResponse.cls
File metadata and controls
184 lines (166 loc) · 6.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "clsHttpResponse"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
' clsHttpResponse - return value of every REST call (sync or async).
'
' Status : HTTP status (e.g. 200, 404). 0 if the request never completed.
' StatusText : HTTP reason phrase.
' Headers : Scripting.Dictionary, case-insensitive header names -> raw values.
' Body : Response body as a VBA String. Always decoded as UTF-8.
' Json : Lazily parsed body. Dictionary / Collection / primitive.
' Returns Empty when the body is empty.
' IsSuccess : 2xx status and no transport error.
' ErrorMessage : Populated on transport errors or JSON parse failures.
Private m_status As Long
Private m_statusText As String
Private m_headers As Object ' Scripting.Dictionary
Private m_body As String
Private m_jsonParsed As Boolean
Private m_json As Variant
Private m_errorMessage As String
Private Sub Class_Initialize()
Set m_headers = CreateObject("Scripting.Dictionary")
m_headers.CompareMode = 1 ' TextCompare = case-insensitive
End Sub
'==========================================================================
' Public properties
'==========================================================================
Public Property Get Status() As Long
Status = m_status
End Property
Public Property Get StatusText() As String
StatusText = m_statusText
End Property
Public Property Get Headers() As Object
Set Headers = m_headers
End Property
Public Property Get Body() As String
Body = m_body
End Property
Public Property Get ErrorMessage() As String
ErrorMessage = m_errorMessage
End Property
Public Property Get IsSuccess() As Boolean
IsSuccess = (m_status >= 200 And m_status < 300 And Len(m_errorMessage) = 0)
End Property
' Lazy JSON parse. Returns the parsed value (Dictionary, Collection, or
' primitive) on first access; cached thereafter. Failure to parse
' populates ErrorMessage rather than raising.
Public Property Get Json() As Variant
If Not m_jsonParsed Then
m_jsonParsed = True
If Len(m_body) > 0 Then
' modJson.JsonParse returns a Variant that may contain an
' object (Dictionary / Collection) or a primitive. We can't
' use plain `=` because Let-assigning a Variant-containing-
' object triggers VBA's default-member trap (Dictionary.Item
' / Collection.Item, both of which need an argument) and
' raises runtime 450/13. Try Set first; if the result is a
' primitive Set raises 424 "Object required" and we retry
' with Let.
On Error Resume Next
Err.Clear
Set m_json = modJson.JsonParse(m_body)
Select Case Err.Number
Case 0
' Got an object - already stored.
Case 424
Err.Clear
m_json = modJson.JsonParse(m_body)
If Err.Number <> 0 Then
m_errorMessage = "JSON parse failed: " & Err.Description
Err.Clear
End If
Case Else
m_errorMessage = "JSON parse failed: " & Err.Description
Err.Clear
End Select
On Error GoTo 0
End If
End If
If IsObject(m_json) Then
Set Json = m_json
Else
Json = m_json
End If
End Property
'==========================================================================
' Internal population (called by the request classes)
'==========================================================================
' Populate the response from a completed MSXML2.XMLHTTP60 or ServerXMLHTTP60.
' Reads responseBody as a byte array and decodes UTF-8 via ADODB.Stream
' (sidesteps MSXML's charset-guessing of responseText). Late-bound so
' the library doesn't require an MSXML type-library reference.
Public Sub LoadFromMSXML(ByVal http As Object)
On Error Resume Next
m_status = http.Status
m_statusText = http.statusText
ParseHeaders http.getAllResponseHeaders
m_body = DecodeUtf8(http.responseBody)
If Err.Number <> 0 Then
m_errorMessage = "Failed to read response: " & Err.Description
Err.Clear
End If
On Error GoTo 0
End Sub
Public Sub SetTransportError(ByVal message As String)
m_errorMessage = message
End Sub
'==========================================================================
' Internals
'==========================================================================
Private Sub ParseHeaders(ByVal raw As String)
If Len(raw) = 0 Then Exit Sub
Dim lines() As String
lines = Split(Replace(raw, vbCrLf, vbLf), vbLf)
Dim i As Long
For i = LBound(lines) To UBound(lines)
Dim line As String
line = lines(i)
If Len(line) > 0 Then
Dim colonAt As Long
colonAt = InStr(line, ":")
If colonAt > 1 Then
Dim k As String, v As String
k = Trim$(Left$(line, colonAt - 1))
v = Trim$(Mid$(line, colonAt + 1))
If m_headers.Exists(k) Then
m_headers(k) = m_headers(k) & ", " & v
Else
m_headers.Add k, v
End If
End If
End If
Next i
End Sub
' Decode a byte array (from MSXML.responseBody) as UTF-8.
' Empty body returns "".
Private Function DecodeUtf8(ByVal bytes As Variant) As String
If IsEmpty(bytes) Then Exit Function
On Error Resume Next
Dim ub As Long
ub = UBound(bytes)
If Err.Number <> 0 Then
Err.Clear
Exit Function
End If
On Error GoTo 0
If ub < 0 Then Exit Function
Dim stm As Object
Set stm = CreateObject("ADODB.Stream")
stm.Type = 1 ' adTypeBinary
stm.Open
stm.Write bytes
stm.Position = 0
stm.Type = 2 ' adTypeText
stm.Charset = "utf-8"
DecodeUtf8 = stm.ReadText
stm.Close
End Function