Hey Dave,

I encountered similar issues with 'text/*' ( text/plain, text/csv, etc) Content-Type header values when I included them in the v4 signed canonical request, and found (in my case) that this is due to Caché appending the charset based on the instance's localization settings (string "; charset=UTF-8") for this header value, eg "Content-Type=text/csv; charset=UTF-8". That likely means you're passing "text/csv" into your signing algorithm to derive your signature but the actual request you're sending has "text/csv; charset=UTF-8", hence the signature failure on the AWS end.

As per this doc,

  • The ContentCharset property controls the desired character set for any content of the request if the content is of type text (text/html or text/xml for example). If you do not specify this property, Caché uses the default encoding of the Caché server.

    Note:

    If you set this property, you must first set the ContentType property.

  • The NoDefaultContentCharset property controls whether to include an explicit character set for content of type text if you have not set the ContentCharset property. By default, this property is false.

Since this isn't one of the headers that needs to be included in AWS v4 signing for the authorisation header,  I just removed this from the list to sign (while still passing it in the actual HTTP request) so I could retain the default charset without too much fuss (I'm not too worried about an attack here, since I'm signing payloads), but the other options (as far as I can tell) would be to pass the same full string to your signing function (including the charset), or prevent charset from being included in Content-Type via that NoDefaultContentCharset property.

Cheers

Joe

Hi Eduard,

I came across this post from 2 years ago when trying to work out the simplest way to get current RFC 822/1123 HTTP-Date values from current timestamps for a request header value (eg the %Net.HttpRequest 'Date' property) in something I'm working on.

At the risk of telling you something you've found out elsewhere since then, it turns out there's already a classmethod that will output this (in GMT) from $horolog (or any $horolog-style ddddd,sssss value, with the assumption it's in local time/date):

write ##class(%CSP.StreamServer).ToHTTPDate($horolog)
>Fri, 18 Jan 2019 04:09:44 GMT

write $zdatetime($horolog,2)
>18 Jan 2019 15:09:44  //AEST timestamp

The Documatic details are here.

Cheers

Joe

This is great stuff, many thanks Fabio.

Is there a way to get child tables created as array property projections (due to a property in a persistent class that's 'as array of' a %SerialObject class) to inherit  the generated trigger, as well as the parent persistent class inheriting this? Or is there a way otherwise to capture the result of inserts/updates/deletes into member rows of these collections via the generated trigger call in %SQLAfterTriggers?  

For direct 'as [%SerialObject class]' properties (not collection 'as array of'), this generated 'elseif property type=serial' logic works for me to create entries in a child audit table devoted to fieldname, old value, new value (note it checks disk against old, as pNew values still match pOld for the serial objects at this point in the aftersave):

 While tKey '= "" {
    set tColumnNbr = $Get($$$EXTPROPsqlcolumnnumber($$$pEXT,%classname,tProperty.Name))
    set tColumnName = $Get($$$EXTPROPsqlcolumnname($$$pEXT,%classname,tProperty.Name)) 
    set tPropertyType = $Get($$$EXTPROPtype($$$pEXT,%classname,tProperty.Name))
    set tPropertyTypeCategory = $Get($$$EXTPROPtypecategory($$$pEXT,%classname,tProperty.Name))
    set tPropertyOnDisk = $Get($$$EXTPROPondisk($$$pEXT,%classname,tProperty.Name))
    set tPropertyOnDisk = $replace(tPropertyOnDisk,"(id)","({id})")

    If tColumnNbr '= "" {
        Do %code.WriteLine($Char(9,9,9)_"if {" _ tProperty.SqlFieldName _ "*C} {") 
 
                 //%code.WriteLine logic from your example

        Do %code.WriteLine($Char(9,9,9)_"}elseif ("""_tPropertyTypeCategory_"""=""serial"") { ")
        Do %code.WriteLine($Char(9,9,9,9)_"if ##class(%Dictionary.CompiledClass).%ExistsId("""_tPropertyType_""") {")
        Do %code.WriteLine($Char(9,9,9,9,9)_"set (tOld,tNew)="""",(ptr,tChildCounter)=0")
        Do %code.WriteLine($Char(9,9,9,9,9)_"set tChildList = "_tPropertyOnDisk)
        Do %code.WriteLine($Char(9,9,9,9,9)_"if $listvalid(tChildList) {")
        Do %code.WriteLine($Char(9,9,9,9,9,9)_"while $listnext(tChildList,ptr,value) {")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9)_"set tChildCounter=tChildCounter+1")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9)_"set tNew(tChildCounter)=value")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9)_"set tOld(tChildCounter)=$listget({"_tProperty.SqlFieldName_"*O},tChildCounter)")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9)_"if tOld(tChildCounter)'=tNew(tChildCounter) {")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9)_"set tChildObj=##class(%Dictionary.CompiledClass).%OpenId("""_tPropertyType_""")")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9)_"if $ISOBJECT(tChildObj) {")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9,9)_"Set tAuditDelta = ##class(Sample.AuditDelta).%New(ParentId)") 
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9,9)_"do tAuditDelta.AuditParentSetObjectId(ParentId)")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9,9)_"set tAuditDelta.AuditDeltaField = tChildObj.Properties.GetObjectIdAt((tChildCounter+1))")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9,9)_"set tAuditDelta.AuditDeltaOldValue = tOld(tChildCounter)")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9,9)_"set tAuditDelta.AuditDeltaNewValue = tNew(tChildCounter)")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9,9)_"set tSC = $$$ADDSC(tSC,tAuditDelta.%Save())") 
        D%code.WriteLine($Char(9,9,9,9,9,9,9,9,9)_"if $$$ISERR(tSC) $$$ThrowStatus(tSC)") 
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9)_"}")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9,9)_"kill tChildObj")
        Do %code.WriteLine($Char(9,9,9,9,9,9,9)_"}")
        Do %code.WriteLine($Char(9,9,9,9,9,9)_"}")
        Do %code.WriteLine($Char(9,9,9,9,9)_"}")  
        Do %code.WriteLine($Char(9,9,9,9)_"}")
        Do %code.WriteLine($Char(9,9,9)_"}") 

 

I kept the if-elseif in the generated code (rather than generating conditionally) because object %Save satisfies the trigger syntax *C (pChanged) check, but SQL update to properties in the serial object doesn't in my tests (the former dumps the full serial object list from the pOld and pNew arrays  into the child audit table's old value and new value fields, versus the serial list creating a child entry for each field updated in the serial object).  The GetObjectIdAt(counter+1) isn't really feasible based on key not always equaling list location, but it's a place holder until I refine it.