Skip to content

Commit c1d2e7a

Browse files
committed
Added compatibility layer for deprecated statusText attribute
- Create cfHeaderUtils.cfc to detect CF version and conditionally use statusText - Add cfHeaderHelper.cfm with global setTaffyStatusHeader() function - Update all cfheader calls to use the new compatibility layer - Add comprehensive test coverage for version detection and header setting
1 parent 69e7f1d commit c1d2e7a

File tree

9 files changed

+414
-11
lines changed

9 files changed

+414
-11
lines changed

bonus/LogToScreen.cfc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
<cffunction name="saveLog">
1010
<cfargument name="exception" />
1111
<cfcontent type="text/html" />
12-
<cfheader statuscode="500" />
12+
<cfinclude template="../core/cfHeaderHelper.cfm" />
13+
<cfset setTaffyStatusHeader(500, "Unhandled API Error") />
1314
<cfdump var="#arguments#" />
1415
<cfif isDefined('request.debugData')>
1516
<cfdump var="#request.debugData#" label="debug data" />

core/api.cfc

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
<cfcomponent hint="Base class for taffy REST application's Application.cfc">
22

3+
<!--- Instance-level utility for backwards compatible cfheader handling --->
4+
<cfset variables.headerUtils = "" />
5+
6+
<!--- Lazy-loaded getter for header utility --->
7+
<cffunction name="getHeaderUtils" access="private" output="false" returntype="any" hint="Returns header utility instance with lazy loading">
8+
<cfif variables.headerUtils eq "">
9+
<cfset variables.headerUtils = createObject("component", "taffy.core.cfHeaderUtils").init() />
10+
</cfif>
11+
<cfreturn variables.headerUtils />
12+
</cffunction>
13+
14+
<!--- Wrapper function to maintain API compatibility --->
15+
<cffunction name="setStatusHeader" access="private" output="false" returntype="void" hint="Sets HTTP status header with backwards compatibility">
16+
<cfargument name="statusCode" type="numeric" required="true" hint="HTTP status code to set" />
17+
<cfargument name="statusText" type="string" required="false" default="" hint="HTTP status text (optional for CF 2025+)" />
18+
<cfset getHeaderUtils().setStatusHeader(arguments.statusCode, arguments.statusText) />
19+
</cffunction>
20+
321
<!--- this method is meant to be (optionally) overrided in your application.cfc --->
422
<cffunction name="getEnvironment" output="false" hint="override this function to define the current API environment"><cfreturn "" /></cffunction>
523

@@ -146,7 +164,7 @@
146164
<cfset logger.saveLog(exception) />
147165

148166
<!--- return 500 no matter what --->
149-
<cfheader statuscode="500" />
167+
<cfset setStatusHeader(500, "Error") />
150168
<cfcontent reset="true" />
151169

152170
<cfif structKeyExists(exception, "rootCause")>
@@ -177,7 +195,7 @@
177195
</cfif>
178196
<cfcatch>
179197
<cfcontent reset="true" type="text/plain; charset=utf-8" />
180-
<cfheader statuscode="500" />
198+
<cfset setStatusHeader(500, "Error") />
181199
<cfoutput>An unhandled exception occurred: <cfif isStruct(root) and structKeyExists(root,"message")>#root.message#<cfelse>#root#</cfif> <cfif isStruct(root) and structKeyExists(root,"detail")>-- #root.detail#</cfif></cfoutput>
182200
<cfdump var="#cfcatch#" format="text" label="ERROR WHEN LOGGING EXCEPTION" />
183201
<cfdump var="#exception#" format="text" label="ORIGINAL EXCEPTION" />
@@ -422,7 +440,7 @@
422440

423441
<cfsetting enablecfoutputonly="true" />
424442
<cfcontent reset="true" type="#getReturnMimeAsHeader(_taffyRequest.returnMimeExt)#; charset=utf-8" />
425-
<cfheader statuscode="#_taffyRequest.statusArgs.statusCode#" />
443+
<cfset setStatusHeader(_taffyRequest.statusArgs.statusCode, _taffyRequest.statusArgs.statusText) />
426444

427445
<!--- headers --->
428446
<cfset addHeaders(_taffyRequest.resultHeaders) />
@@ -498,7 +516,7 @@
498516
<cfset _taffyRequest.clientEtag = _taffyRequest.headers['If-None-Match'] />
499517

500518
<cfif len(_taffyRequest.clientEtag) gt 0 and _taffyRequest.clientEtag eq _taffyRequest.serverEtag>
501-
<cfheader statuscode="304" />
519+
<cfset setStatusHeader(304, "Not Modified") />
502520
<cfcontent reset="true" type="#application._taffy.settings.mimeExtensions[_taffyRequest.returnMimeExt]#; charset=utf-8" />
503521
<cfreturn true />
504522
<cfelse>
@@ -1117,7 +1135,7 @@
11171135
<cfargument name="headers" type="struct" required="false" default="#structNew()#" />
11181136
<cfcontent reset="true" />
11191137
<cfset addHeaders(arguments.headers) />
1120-
<cfheader statuscode="#arguments.statusCode#" />
1138+
<cfset setStatusHeader(arguments.statusCode, arguments.msg) />
11211139
<cfabort />
11221140
</cffunction>
11231141

core/baseDeserializer.cfc

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,24 @@
3434
<!--- Helpers --->
3535
<!--- ============================ --->
3636

37+
<!--- Instance-level utility for backwards compatible cfheader handling --->
38+
<cfset variables.headerUtils = "" />
39+
40+
<!--- Lazy-loaded getter for header utility --->
41+
<cffunction name="getHeaderUtils" access="private" output="false" returntype="any" hint="Returns header utility instance with lazy loading">
42+
<cfif variables.headerUtils eq "">
43+
<cfset variables.headerUtils = createObject("component", "taffy.core.cfHeaderUtils").init() />
44+
</cfif>
45+
<cfreturn variables.headerUtils />
46+
</cffunction>
47+
3748
<cffunction name="throwError" access="private" output="false" returntype="void">
38-
<cfargument name="statusCode" type="numeric" default="500" />
49+
<cfargument name="statusCode" type="numeric" default="500" hint="HTTP status code to return" />
3950
<cfargument name="msg" type="string" required="true" hint="message to return to api consumer" />
40-
<cfargument name="headers" type="struct" required="false" default="#structNew()#" />
51+
<cfargument name="headers" type="struct" required="false" default="#structNew()#" hint="additional HTTP headers" />
4152
<cfcontent reset="true" />
4253
<cfset addHeaders(arguments.headers) />
43-
<cfheader statuscode="#arguments.statusCode#" />
54+
<cfset getHeaderUtils().setStatusHeader(arguments.statusCode, arguments.msg) />
4455
<cfabort />
4556
</cffunction>
4657

core/cfHeaderHelper.cfm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!---
2+
Global utility function wrapper for backwards compatible cfheader handling.
3+
4+
This provides a simple function interface that wraps the cfHeaderUtils.cfc component.
5+
The CFC is cached in application scope for performance. This helper allows code to call
6+
setTaffyStatusHeader() directly without managing component instances.
7+
--->
8+
9+
<cffunction name="setTaffyStatusHeader" output="false" returntype="void" hint="Sets HTTP status header with backwards compatibility for CF 2025">
10+
<cfargument name="statusCode" type="numeric" required="true" hint="HTTP status code to set" />
11+
<cfargument name="statusText" type="string" required="false" default="" hint="HTTP status text (optional for CF 2025+)" />
12+
13+
<cfscript>
14+
// Use application scope to cache the utility instance
15+
if (!structKeyExists(application, "_taffyHeaderUtils")) {
16+
application._taffyHeaderUtils = createObject("component", "taffy.core.cfHeaderUtils").init();
17+
}
18+
19+
application._taffyHeaderUtils.setStatusHeader(arguments.statusCode, arguments.statusText);
20+
</cfscript>
21+
</cffunction>

core/cfHeaderUtils.cfc

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!---
2+
Core component class for backwards compatible HTTP status header handling for ColdFusion 2025+
3+
where the statustext attribute has been deprecated.
4+
5+
This CFC provides the structured, testable implementation with dependency injection support.
6+
For a simple global function interface, see cfHeaderHelper.cfm which wraps this component.
7+
--->
8+
<cfcomponent hint="Utility component for backwards compatible cfheader handling">
9+
10+
<!--- Cache the CF version check since it won't change during execution --->
11+
<cfset variables.isCF2025OrLater = "" />
12+
<cfset variables.serverInfo = "" />
13+
14+
<!--- Constructor to inject server information --->
15+
<cffunction name="init" access="public" output="false" returntype="any" hint="Constructor with optional server info injection">
16+
<cfargument name="serverInfo" type="struct" required="false" default="#structNew()#" hint="Server info for testing/injection" />
17+
18+
<cfscript>
19+
if (structIsEmpty(arguments.serverInfo)) {
20+
// Only reference server scope if no injection provided
21+
variables.serverInfo = server;
22+
} else {
23+
variables.serverInfo = arguments.serverInfo;
24+
}
25+
</cfscript>
26+
27+
<cfreturn this />
28+
</cffunction>
29+
30+
<cffunction name="setStatusHeader" access="public" output="false" returntype="void" hint="Sets HTTP status header with backwards compatibility for CF 2025">
31+
<cfargument name="statusCode" type="numeric" required="true" hint="HTTP status code to set" />
32+
<cfargument name="statusText" type="string" required="false" default="" hint="HTTP status text (optional for CF 2025+)" />
33+
34+
<cfscript>
35+
if (isColdFusion2025OrLater()) {
36+
cfheader(statuscode=arguments.statusCode);
37+
} else {
38+
if (len(arguments.statusText)) {
39+
cfheader(statuscode=arguments.statusCode, statustext=arguments.statusText);
40+
} else {
41+
cfheader(statuscode=arguments.statusCode);
42+
}
43+
}
44+
</cfscript>
45+
</cffunction>
46+
47+
<cffunction name="isColdFusion2025OrLater" access="public" output="false" returntype="boolean" hint="Detects if running ColdFusion 2025 or later">
48+
<cfscript>
49+
// Cache the result since CF version won't change during execution
50+
if (variables.isCF2025OrLater == "") {
51+
var cfVersion = 0;
52+
if (structKeyExists(variables.serverInfo, "coldfusion") &&
53+
structKeyExists(variables.serverInfo.coldfusion, "productname") &&
54+
variables.serverInfo.coldfusion.productname contains "ColdFusion") {
55+
cfVersion = val(listFirst(variables.serverInfo.coldfusion.productversion));
56+
}
57+
variables.isCF2025OrLater = (cfVersion >= 2025);
58+
}
59+
return variables.isCF2025OrLater;
60+
</cfscript>
61+
</cffunction>
62+
63+
</cfcomponent>

dashboard/asset.cfm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
</cfcase>
2929

3030
<cfdefaultcase>
31-
<cfheader statuscode="404" />
31+
<cfinclude template="../core/cfHeaderHelper.cfm" />
32+
<cfset setTaffyStatusHeader(404, "Not Found") />
3233
<cfcontent reset="true" /><cfabort />
3334
</cfdefaultcase>
3435

tests/tests/TestCfHeaderUtils.cfc

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
component extends="base" {
2+
3+
function beforeTests() {
4+
variables.headerUtils = "";
5+
}
6+
7+
function setup() {
8+
// Create fresh instance for each test
9+
variables.headerUtils = createObject("component", "taffy.core.cfHeaderUtils");
10+
}
11+
12+
// Test constructor with no server info (will use actual server scope)
13+
function test_init_with_no_server_info() {
14+
var result = variables.headerUtils.init();
15+
assertEquals(variables.headerUtils, result, "init() should return this");
16+
}
17+
18+
// Test constructor with mock server info for CF 2024
19+
function test_init_with_cf2024_server_info() {
20+
var mockServerInfo = {
21+
coldfusion: {
22+
productname: "ColdFusion",
23+
productversion: "2024.0.0"
24+
}
25+
};
26+
27+
var result = variables.headerUtils.init(mockServerInfo);
28+
assertEquals(variables.headerUtils, result, "init() should return this");
29+
assertEquals(false, result.isColdFusion2025OrLater(), "CF 2024 should not be detected as 2025+");
30+
}
31+
32+
// Test constructor with mock server info for CF 2025
33+
function test_init_with_cf2025_server_info() {
34+
var mockServerInfo = {
35+
coldfusion: {
36+
productname: "ColdFusion",
37+
productversion: "2025.0.0"
38+
}
39+
};
40+
41+
var result = variables.headerUtils.init(mockServerInfo);
42+
assertEquals(variables.headerUtils, result, "init() should return this");
43+
assertEquals(true, result.isColdFusion2025OrLater(), "CF 2025 should be detected as 2025+");
44+
}
45+
46+
// Test constructor with mock server info for CF 2026
47+
function test_init_with_cf2026_server_info() {
48+
var mockServerInfo = {
49+
coldfusion: {
50+
productname: "ColdFusion",
51+
productversion: "2026.0.0"
52+
}
53+
};
54+
55+
var result = variables.headerUtils.init(mockServerInfo);
56+
assertEquals(true, result.isColdFusion2025OrLater(), "CF 2026 should be detected as 2025+");
57+
}
58+
59+
// Test with Lucee server info (should not be detected as CF 2025+)
60+
function test_init_with_lucee_server_info() {
61+
var mockServerInfo = {
62+
lucee: {
63+
version: "5.3.8.206"
64+
},
65+
coldfusion: {
66+
productname: "Lucee",
67+
productversion: "5.3.8"
68+
}
69+
};
70+
71+
var result = variables.headerUtils.init(mockServerInfo);
72+
assertEquals(false, result.isColdFusion2025OrLater(), "Lucee should not be detected as CF 2025+");
73+
}
74+
75+
// Test with non-ColdFusion server info
76+
function test_init_with_non_cf_server_info() {
77+
var mockServerInfo = {
78+
os: {
79+
name: "Windows"
80+
}
81+
};
82+
83+
var result = variables.headerUtils.init(mockServerInfo);
84+
assertEquals(false, result.isColdFusion2025OrLater(), "Non-CF server should not be detected as CF 2025+");
85+
}
86+
87+
// Test version detection caching
88+
function test_version_detection_caching() {
89+
var mockServerInfo = {
90+
coldfusion: {
91+
productname: "ColdFusion",
92+
productversion: "2025.0.0"
93+
}
94+
};
95+
96+
var util = variables.headerUtils.init(mockServerInfo);
97+
98+
// First call
99+
var result1 = util.isColdFusion2025OrLater();
100+
// Second call should use cached result
101+
var result2 = util.isColdFusion2025OrLater();
102+
103+
assertEquals(result1, result2, "Cached version detection should return same result");
104+
assertEquals(true, result1, "CF 2025 should be detected correctly");
105+
}
106+
107+
// Test setStatusHeader method (we can't easily test the actual cfheader call,
108+
// but we can test that the method executes without error)
109+
function test_setStatusHeader_with_cf2024() {
110+
var mockServerInfo = {
111+
coldfusion: {
112+
productname: "ColdFusion",
113+
productversion: "2024.0.0"
114+
}
115+
};
116+
117+
var util = variables.headerUtils.init(mockServerInfo);
118+
119+
// This should execute without throwing an exception
120+
// Note: We can't easily test the actual cfheader output in unit tests
121+
try {
122+
util.setStatusHeader(500, "Test Error");
123+
// If we get here, the method executed without error
124+
assertTrue(true, "setStatusHeader should execute without error for CF 2024");
125+
} catch (any e) {
126+
fail("setStatusHeader should not throw exception: " & e.message);
127+
}
128+
}
129+
130+
function test_setStatusHeader_with_cf2025() {
131+
var mockServerInfo = {
132+
coldfusion: {
133+
productname: "ColdFusion",
134+
productversion: "2025.0.0"
135+
}
136+
};
137+
138+
var util = variables.headerUtils.init(mockServerInfo);
139+
140+
try {
141+
util.setStatusHeader(404, "Not Found");
142+
assertTrue(true, "setStatusHeader should execute without error for CF 2025");
143+
} catch (any e) {
144+
fail("setStatusHeader should not throw exception: " & e.message);
145+
}
146+
}
147+
148+
function test_setStatusHeader_without_statustext() {
149+
var mockServerInfo = {
150+
coldfusion: {
151+
productname: "ColdFusion",
152+
productversion: "2024.0.0"
153+
}
154+
};
155+
156+
var util = variables.headerUtils.init(mockServerInfo);
157+
158+
try {
159+
util.setStatusHeader(200);
160+
assertTrue(true, "setStatusHeader should work without statusText parameter");
161+
} catch (any e) {
162+
fail("setStatusHeader should not throw exception when statusText is empty: " & e.message);
163+
}
164+
}
165+
166+
// Test edge case version strings
167+
function test_version_detection_with_complex_version() {
168+
var mockServerInfo = {
169+
coldfusion: {
170+
productname: "ColdFusion",
171+
productversion: "2025.1.2.3-UPDATE1"
172+
}
173+
};
174+
175+
var util = variables.headerUtils.init(mockServerInfo);
176+
assertEquals(true, util.isColdFusion2025OrLater(), "Should handle complex version strings");
177+
}
178+
179+
function test_version_detection_with_cf11() {
180+
var mockServerInfo = {
181+
coldfusion: {
182+
productname: "ColdFusion",
183+
productversion: "11.0.0"
184+
}
185+
};
186+
187+
var util = variables.headerUtils.init(mockServerInfo);
188+
assertEquals(false, util.isColdFusion2025OrLater(), "CF 11 should not be detected as 2025+");
189+
}
190+
191+
192+
}

0 commit comments

Comments
 (0)