Six new features (from my internal fork) (+1 CF2016 bugfix)#455
Six new features (from my internal fork) (+1 CF2016 bugfix)#455JamoCA wants to merge 8 commits intoatuttle:mainfrom
Conversation
qToArray built each row as a plain struct, so the key order depended on the CFML engine's hash order. Switching the row literal to [:] gives an ordered struct, which keeps the column order you see in the database or in the SELECT list. Callers that serialize rows to JSON now get predictable field order for free.
…t mime When a browser loads an API URL directly, its `Accept` header lists `text/html`, several image types, and `application/signed-exchange` before it ever gets to `*/*`. Taffy currently walks that list looking for a match in `mimeTypes`, fails to find one, and falls through to `listFirst(headers.accept, ",")`, which is `text/html`. The API then blows up with "Requested format not available (text/html)". This patch adds a cheap pre-check: if the three browser-ish signals are all present in the header, serve the configured default mime and skip negotiation.
…ies through Four related tweaks to how `parseRequest` decides what to do with a request body. Clients that forget a `Content-Type`, logs that arrive one JSON value per line, browser CSP reports, and standard HTML form posts are all things we hit often enough that returning 400 or double-deserializing is the wrong default. Each case routes to the right deserializer (or bypasses it) without the caller having to set a header.
Taffy currently calls `cfheader(...)` from eighteen places in `api.cfc`. If any code upstream has already flushed the response buffer (a streamed `cfcontent`, an early `cfflush`, a load balancer probe that closed the connection, a WAF that sent an `Expect: 100-continue`) those `cfheader` calls are silent no-ops at best and hard exceptions at worst. This patch funnels every header write through one `addHeader()` helper that checks `isFlushed()` first, and aggregates the cfheader attributes into a single `attributeCollection` call so a statusCode+statusText pair goes out in one tag. Same output on the happy path; no more "Response already committed" when the buffer is gone.
Two dashboard QoL changes. The resource filter input becomes a real <input type="search">, which gives browsers a free clear-X button and (in most engines) the little magnifying-glass affordance. The JSON response viewer gains a "CLICK TO DUMP" button that opens the parsed response in a popup rendered by cfDump.js, a dependency-free CFML-style nested table viewer. cfDump is bundled with the port (dashboard/cfDump.min.js, MIT-licensed).
Required for CF2016.
Two dashboard QoL changes. The resource filter input becomes a real <input type="search">, which gives browsers a free clear-X button and (in most engines) the little magnifying-glass affordance. The JSON response viewer gains a "CLICK TO DUMP" button that opens the parsed response in a popup rendered by cfDump.js, a dependency-free CFML-style nested table viewer. cfDump is bundled with the port (dashboard/cfDump.min.js, MIT-licensed).
…sponses Adds a `noCache()` method to `taffy.core.baseSerializer` that stamps the four classic cache-busting headers (`Pragma`, `Cache-Control`, `Last-Modified`, `Expires`) onto the response. Useful on GET endpoints whose payload changes per-request (rate-limited views, dashboards, anything with a Set-Cookie or per-user data) where browsers and intermediaries would otherwise serve a stale 304 or a cached copy. Chainable like the rest of the serializer API, so a resource can write `return rep(data).noCache();`.
✅ Deploy Preview for taffy-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Could a maintainer add the Semver-Patch label? |
|
Thanks for the submission. I'm reviewing as time permits. |
| // Body-based content-type inference: fill in contentType when the client didn't send one | ||
| // but the body is obviously JSON, or when the body is newline-delimited JSON (ndJSON). | ||
| if (!len(requestObj.contentType) && isSimpleValue(requestObj.body) && isJSON(requestObj.body)) { | ||
| requestObj.contentType = "application/json"; | ||
| } else if ( | ||
| isSimpleValue(requestObj.body) | ||
| && listLen(requestObj.body, chr(10)) > 1 | ||
| && !isJSON(requestObj.body) | ||
| && isJSON("[" & javacast("string", requestObj.body).replace(chr(10), ",") & "]") | ||
| ) { | ||
| // ndJSON: one JSON value per line. Wrap the lines into a single JSON array before handing off. | ||
| requestObj.body = "[" & javacast("string", requestObj.body).replace(chr(10), ",") & "]"; | ||
| requestObj.contentType = "application/json"; | ||
| } |
There was a problem hiding this comment.
I have some concerns about this section:
- Possible performance issues for large payloads. Even worse if it fails both JSON + ndJSON checks, because it attempted to infer both, requiring managing large strings, and got no benefit from it.
- Bugs: what about
\r\n? What about a payload that ends with an\n(or\r\n)?isJson([{},]throws on ACF (not on Lucee).
I'm not convinced that inferring contentType is worth adding when it could add a lot of performance overhead.
| } else if ( | ||
| structKeyExists(arguments.headers, "Accept") | ||
| && findNoCase("text/html", arguments.headers.accept) | ||
| && findNoCase("image/", arguments.headers.accept) | ||
| && findNoCase("application/signed", arguments.headers.accept) | ||
| ) { | ||
| // Browser-style Accept header (html + image/* + signed-exchange). Skip content negotiation | ||
| // and serve the API's configured default mime instead of trying to match text/html. | ||
| local.returnData["_taffy_mime"] = application._taffy.settings.defaultMime; |
There was a problem hiding this comment.
This is an interesting idea, but application/signed-exchange is a Chromium thing. Firefox sends text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8. Safari similar. Firefox users still get 400. If the goal is "browser opened the URL directly," I think checking for text/html + */* would be sufficient.
| // site can supply; see README. | ||
| var renderBtn = ''; | ||
| if (resource.__taffyJsonData !== null && typeof window.taffyDump === 'function') { | ||
| renderBtn = ' <span class="dumpBtn label label-default" style="cursor:pointer; margin-left:0.5em;">CLICK TO DUMP</span>'; |
There was a problem hiding this comment.
why a span and not a <button>? There's some a11y concerns around this, some of which would be easily solved by switching to a button.
| <cfinclude template="cfDump.min.js" /> | ||
| // adapter so dash.js can call window.taffyDump(data) without knowing about cfDump | ||
| window.taffyDump = function(data){ cfDump(data, true, true, true); }; |
There was a problem hiding this comment.
I'm not sold on adding this in an on-by-default approach, but I think I'm coming around on including the file and using a config setting to turn it on (off by default).
Please Ignore "search-type filter input and JSON response dump (cfDump.js)"
queryToArray ordered rows (core/resource.cfc)
Switch the row literal in qToArray from {} to [:] so JSON output keeps column order on Lucee 5+ / ACF 2021+.
Browser-Accept short-circuit (core/api.cfc)
buildRequestArguments returns the configured defaultMime when the Accept header contains text/html + image/* + application/signed. Stops the "Requested format not available (text/html)" 400 when an API URL is opened directly in a browser.
parseRequest content-type handling (core/api.cfc)
Infer application/json when the body parses as JSON but no header was sent. Accept newline-delimited JSON by wrapping lines into a single array. Treat application/csp-report as JSON. Skip the deserializer for application/x-www-form-urlencoded so the engine's form scope merge handles it (matching the existing multipart path).
cfheader flush-gate (core/api.cfc)
Add private isFlushed() and public addHeader(name,value,statusCode, charset,statusText) helpers. addHeader checks isFlushed first and aggregates attributes into one cfheader(attributeCollection=...) call. All eighteen direct cfheader call sites in api.cfc are routed through addHeader; addHeaders and addTaffyHeader call through too. Headers written after the buffer commits are dropped silently instead of throwing "Response already committed".
Dashboard: search input + JSON response dump
(dashboard/dashboard.cfm, dashboard/dash.js, dashboard/asset.cfm, dashboard/cfDump.min.js + .map + cfDump.LICENSE) Resource filter input becomes for the native clear-X button; a 'search' event listener mirrors ESC handling so clicking the X re-runs the filter. A "CLICK TO DUMP" badge renders next to the response status code on JSON responses and calls window.taffyDump(data). Bundles cfDump.js (MIT, James Moberg) as the default renderer; sites can override window.taffyDump to swap in any other handler. asset.cfm gains cases for cfDump.min.js and its sourcemap.
baseSerializer.noCache() (core/baseSerializer.cfc)
Chainable serializer method that stamps Pragma, Cache-Control (no-cache, no-store, must-revalidate), a current Last-Modified, and the historical Expires past-date directly into miscHeaders. Resources call
return rep(data).noCache();. Fixes the latent bug in the 3.7.0 custom version that called an undefined addHeaders() from inside the serializer.