-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[core-rest-pipeline] handle x-www-form-urlencoded
form data
#18560
[core-rest-pipeline] handle x-www-form-urlencoded
form data
#18560
Conversation
Currently `formDataPolicy` doesn't support x-www-form-urlencoded form data. Sending request with such form data would cause `write EPIPE` error. This PR adds handling of `x-www-form-urlencoded` form data. With this the workaround in container registry library can be removed.
making it a draft PR for now. Will not merge before releasing ACR RC next week. |
This pull request is protected by Check Enforcer. What is Check Enforcer?Check Enforcer helps ensure all pull requests are covered by at least one check-run (typically an Azure Pipeline). When all check-runs associated with this pull request pass then Check Enforcer itself will pass. Why am I getting this message?You are getting this message because Check Enforcer did not detect any check-runs being associated with this pull request within five minutes. This may indicate that your pull request is not covered by any pipelines and so Check Enforcer is correctly blocking the pull request being merged. What should I do now?If the check-enforcer check-run is not passing and all other check-runs associated with this PR are passing (excluding license-cla) then you could try telling Check Enforcer to evaluate your pull request again. You can do this by adding a comment to this pull request as follows: What if I am onboarding a new service?Often, new services do not have validation pipelines associated with them, in order to bootstrap pipelines for a new service, you can issue the following command as a pull request comment: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding this support! It's strange that we never needed it in core-http, but I'm glad we're adding it now.
I think there is a simpler way than doing the urlencoding manually -- namely using URLSearchParams to do the heavy lifting. See the comment I left in the browser version of the policy. I think we can do the same thing in node.
* @returns result of form data encoded for application/x-www-form-urlencoded content type. | ||
* See https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4 for more details. | ||
*/ | ||
export function urlEncode(formData: FormDataMap) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we name this something that indicates it is only for form data? e.g.
export function urlEncode(formData: FormDataMap) { | |
export function urlEncodeFormData(formData: FormDataMap) { |
const result = await policy.sendRequest(request, next); | ||
|
||
assert.isUndefined(result.request.formData); | ||
assert.strictEqual(result.request.body?.toString(), "[object FormData]"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this doesn't seem like a very useful assertion - can we grab out the form data to make sure it's what we expect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
form-data's FormData
has these
declare class FormData extends stream.Readable {
constructor(options?: Options);
append(key: string, value: any, options?: FormData.AppendOptions | string): void;
getHeaders(userHeaders?: FormData.Headers): FormData.Headers;
submit(
params: string | FormData.SubmitOptions,
callback?: (error: Error | null, response: http.IncomingMessage) => void
): http.ClientRequest;
getBuffer(): Buffer;
setBoundary(boundary: string): void;
getBoundary(): string;
getLength(callback: (err: Error | null, length: number) => void): void;
getLengthSync(): number;
hasKnownLength(): boolean;
}
which is different from browser's FormData
interface
interface FormData {
append(name: string, value: string | Blob, fileName?: string): void;
delete(name: string): void;
get(name: string): FormDataEntryValue | null;
getAll(name: string): FormDataEntryValue[];
has(name: string): boolean;
set(name: string, value: string | Blob, fileName?: string): void;
forEach(callbackfn: (value: FormDataEntryValue, key: string, parent: FormData) => void, thisArg?: any): void;
}
I guess I will have to do different tests for Node and browser.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The node one supports 'toString', if that helps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not very helpful. it gave "[object FormData]" as in my first version of the test
@@ -17,25 +18,30 @@ export function formDataPolicy(): PipelinePolicy { | |||
name: formDataPolicyName, | |||
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I found a much easier way to handle the encoding that avoids needing us to have a special helper:
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
if (request.formData) {
const formData = request.formData;
const requestForm = new FormData();
for (const formKey of Object.keys(formData)) {
const formValue = formData[formKey];
if (Array.isArray(formValue)) {
for (const subValue of formValue) {
requestForm.append(formKey, subValue);
}
} else {
requestForm.append(formKey, formValue);
}
}
request.body = requestForm;
request.formData = undefined;
const contentType = request.headers.get("Content-Type");
if (contentType && contentType.indexOf("application/x-www-form-urlencoded") !== -1) {
request.body = new URLSearchParams(requestForm).toString();
} else if (contentType && contentType.indexOf("multipart/form-data") !== -1) {
// browser will automatically apply a suitable content-type header
request.headers.delete("Content-Type");
}
}
return next(request);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great! I didn't know that we can construct URLSearchParams with FormData.
I just gave it a try.
|
Yeah I think in the node version we'll have to fork the logic here a little, but it's still easy to take the original formData object and use that: new URLSearchParams(Object.entries(formData)).toString() |
I will probably need to loop and append to URLSearchParams to handle the Array case but we don't need to add a helper method any more |
- Split tests because multi-part verification is different between NodeJs and browser
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice! Thanks for the update. 👍
/azp run js - containerregistry - tests |
Azure Pipelines successfully started running 1 pipeline(s). |
Currently
formDataPolicy
doesn't support x-www-form-urlencoded form data.Sending request with such form data would cause
write EPIPE
error. This PRadds handling of
x-www-form-urlencoded
form data. With this the workaround incontainer registry library can be removed.
Related: #14767