Skip to content

Six new features (from my internal fork) (+1 CF2016 bugfix)#455

Open
JamoCA wants to merge 8 commits intoatuttle:mainfrom
JamoCA:main
Open

Six new features (from my internal fork) (+1 CF2016 bugfix)#455
JamoCA wants to merge 8 commits intoatuttle:mainfrom
JamoCA:main

Conversation

@JamoCA
Copy link
Copy Markdown
Contributor

@JamoCA JamoCA commented Apr 25, 2026

Please Ignore "search-type filter input and JSON response dump (cfDump.js)"

  1. 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+.

  2. 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.

  3. 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).

  4. 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".

  5. 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.

  6. 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.

JamoCA added 8 commits April 24, 2026 11:34
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();`.
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 25, 2026

Deploy Preview for taffy-docs ready!

Name Link
🔨 Latest commit 6032c38
🔍 Latest deploy log https://app.netlify.com/projects/taffy-docs/deploys/69ec28ad7c47b400083252db
😎 Deploy Preview https://deploy-preview-455--taffy-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request does not contain a valid label. Please add one of the following labels: ['Semver-Major', 'Semver-Minor', 'Semver-Patch']

@JamoCA
Copy link
Copy Markdown
Contributor Author

JamoCA commented Apr 25, 2026

Could a maintainer add the Semver-Patch label?

@atuttle
Copy link
Copy Markdown
Owner

atuttle commented Apr 27, 2026

Thanks for the submission. I'm reviewing as time permits.

@atuttle atuttle added the Semver-Minor This change will necessitate a minor version bump label Apr 27, 2026
Comment thread core/api.cfc
Comment on lines +743 to +756
// 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";
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread core/api.cfc
Comment on lines +1049 to +1057
} 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;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread dashboard/dash.js
// 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>';
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread dashboard/dashboard.cfm
Comment on lines +558 to +560
<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); };
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Semver-Minor This change will necessitate a minor version bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants