Skip to content
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

by default not send undefined params in url #20564

Open
2 tasks done
elvisbegovic opened this issue Nov 21, 2017 · 22 comments
Open
2 tasks done

by default not send undefined params in url #20564

elvisbegovic opened this issue Nov 21, 2017 · 22 comments
Labels
area: router breaking changes feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature
Milestone

Comments

@elvisbegovic
Copy link
Contributor

elvisbegovic commented Nov 21, 2017

I'm submitting a...

  • Feature request

CURRENT

To be simple, I will take example for GET request. Here you have a simple method making http call with an optional parameter:

public getCity(cityId?: number){
  this.get('city', { params: {city: cityId}})
}

cityId? is optional, so you can call method like this.getCity(), if you do so angular will make request to this url : http://localhost/city?city=undefined

EXPECTED

I think avoid sending undefined params (not including them in HttpParams if they are undefined) should be most common use case because we can check this on backend. If params aren't present == undefined.

MOTIVATION

Advantages of this is : maybe URL have limits and sending essential/useful data we really need is important. I don't understand why send them by default if they are undefined. Something like Include.NON_NULL in jackson.
What you think about not send them by default ? or add option in options{} object just to avoid this big codes:

let cleanedParams = new HttpParams();
req.params.keys().forEach(x => {
  if(req.params.get(x) != undefined)
    cleanedParams = cleanedParams.append(x, req.params.get(x));
})

const clonedRequest = req.clone({
  params:cleanedParams
});

return next.handle(clonedRequest)

Otherwise yes actually we can fortunately workaround by interceptor or with :

public getCity(cityId?: number){
  let params = {};
  if(cityId)
    params.city=cityId;
  this.get('city', params)
}

But when you have 10/20 params or 500/1k http request you are happy if you avoid this condition :-D

I didn't think about others backend endpoint so will think within my use case. Idea should only react with undefined params not with null params. For instance my endpoint in java if I expect to receive @PathVariable Long cityId I can receive null but not undefined.

Environment

Angular 5.0.2

Browser tested:

  • Chrome console (version 21.11.2017)
@jasonaden jasonaden added the feature Issue that requests a new feature label Nov 21, 2017
@digitalcraftco
Copy link

Also this is useful for making generic HTTP services instead of creating one with 2 params & 3 params etc.

   return this.http.get(url: string, type: string,  page?: any, size?: any) {
    return this.http.get(`${environment.apiEndpoint}${url}`, {
      headers: this.createHeaders(),
      params: { type: type, page: page, size: size },
    }).pipe(
      retry(1),
      catchError(error => this.handleError(error))
    );
  }

// http://xx.xx.xx:xxxx/api/reservation?type=retail&page=undefined&size=undefined

@ngbot ngbot bot added this to the Backlog milestone Jan 23, 2018
@gitfortee
Copy link

@istiti I am in a similar need. Mine is an angular 5 + dotnet core web api application

I believe you might already have a workaround.

Here is my setup looks like. And i get 'undefined' for fName and lName query parameters at my dotnet core Web Api backend. You can assume fName and lName are optional.

    params:{
        firstname:'',
        lastName:''
    }

    this.http.get(`/user?fName=${this.params.firstname}&lName=${this.params.lastName}`);

I even tried this but ended up with same result.

    params:{
        firstname:" ",
        lastName:" "
    }

    this.http.get(`/user?fName=${this.params.firstname}&lName=${this.params.lastName}`);

How are you handling the 'undefined' from being passed to your backend..? I could not find a way to assign an empty string value to my object properties.

Appreciate your input. Thanks!

@trotyl
Copy link
Contributor

trotyl commented Mar 16, 2018

Duplicate of #18567

@rkuodys
Copy link

rkuodys commented Apr 21, 2018

I believe that the way it works now has some inconsistencies:

     let p = new HttpParams({fromObject: {'g' : undefined, 'n' : null}});
      p.set('a', null);
      p.set('c', undefined);
      console.log(p.toString());
      console.log(p.toString(), p.get('a'), p.get('b'), p.get('c'), p.get('g'), p.get('n'));

console prints out: g=undefined&n=null', null, null, null, undefined, null
Inconsistency 1) - it works differently if you pass it as object or by "set"
Inconsistency 2) - how do you konw whether n is in params or not? p.get('n') which is in params returns the same thing as p.get('b') which is not, hence not allowing you to sanitise params manually.

@Mufasa
Copy link

Mufasa commented Jun 9, 2018

Since I use optional parameters quite a bit, I decided to write my own helper function which I have listed below in case it proves useful to others.

In a file named http-params.ts:

import { HttpParams } from '@angular/common/http';

export function createHttpParams(params: {}): HttpParams {
    let httpParams: HttpParams = new HttpParams();
    Object.keys(params).forEach(param => {
        if (params[param]) {
            httpParams = httpParams.set(param, params[param]);
        }
    });

    return httpParams;
}

Then, when I need to create some HTTP parameters, I use it as follows:

import { HttpParams } from '@angular/common/http';
import { createHttpParams } from '../lib/http-params';
...
@Injectable()
export class MyApiService {
  someApiMethod(param1: string, param2?: string, param3?: string): Observable<Member[] | {}> {
    const params: HttpParams = createHttpParams({ param1: param1, param2: param2, param3: param3 });
    ...
  }
...
}

@timtheguy
Copy link

@Mufasa, I found great success in using the helper function you've created after beginning the migration to the Angular 4.4 HttpClient from Angular 2's Http in our code base. Many requests we're making rely on the same optional parameter behavior from the previous URLSearchParams, and are breaking test cases where the new HttpParams returns the 'undefined' value -- rather than 'ignoring' them in the query string. Thanks!

@Mufasa
Copy link

Mufasa commented Jun 14, 2018

@timtheguy I'm glad it came in useful.

@bella-daza
Copy link

bella-daza commented Nov 13, 2018

Thanks for your helper function @Mufasa :) It's very useful!

@muliyul
Copy link

muliyul commented Jan 29, 2019

We too seek that functionality (and believe it should come out of the box)

@kubk
Copy link
Contributor

kubk commented Oct 1, 2019

This definitely should be the default behavior.

@krotscheck
Copy link

For future readers, this can also be handled generically as an HTTP Interceptor:


import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

/**
 * Strip out any `undefined` parameters in the query string.
 */
@Injectable()
export class StripUndefinedParams implements HttpInterceptor {

    /**
     * Iterate through query parameters and remove all those that are `undefined`.
     *
     * @param request The incoming request.
     * @param next The next handler.
     * @returns The handled request.
     */
    public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        let params = request.params;
        for (const key of request.params.keys()) {
            if (params.get(key) === undefined) {
                params = params.delete(key, undefined);
            }
        }
        request = request.clone({ params });
        return next.handle(request);
    }
}

@gxenakis
Copy link

Or if you prefer a function to deal with objects ( multidimensional ) before sending it to HttpClient.

Feel free to suggest for more optimization.

function deleteUndefinedKeysInObject(object){
        let obj = Object.assign({}, object);
        for (let key of Object.keys(obj)) {
            if (obj[key] === undefined) {
                delete obj[key];
            }
            if (Array.isArray(obj[key])){
                if (obj[key].length>0){
                    for(let i=0; i<obj[key].length; i++){
                        console.log(obj[key][i]);
                        obj[key][i] = Object.assign({}, this.deleteUndefinedKeys(obj[key][i]));
                    }
                }
            }
        }
        return obj;
    }

@CyberFoxar
Copy link

Impressive that this still seems to be an issue with Angular 11. Glad people made workaround here at last.

@krotscheck
Copy link

FWIW- I don't think it should be the framework's responsibility to protect us from passing invalid values. If I pass an undefined by accident, I would much rather it show up in the URL so I know exactly what I'm doing wrong. If these values disappear 'magically', and I'm actually expecting them, I face a much harder task tracing the origin.

@petebacondarwin
Copy link
Member

Personally, I think this is a perfect case for an interceptor as suggest in #20564 (comment). Exactly the sort of thing that this extension point was designed for. Then, rather than having it built-in and hidden behind a config option, it is triggered by adding the interceptor or not.

@angular-robot angular-robot bot added the feature: under consideration Feature request for which voting has completed and the request is now under consideration label Jun 4, 2021
@larscom
Copy link

larscom commented Oct 13, 2021

FWIW- I don't think it should be the framework's responsibility to protect us from passing invalid values. If I pass an undefined by accident, I would much rather it show up in the URL so I know exactly what I'm doing wrong. If these values disappear 'magically', and I'm actually expecting them, I face a much harder task tracing the origin.

I would at least expect a property inside HttpParamsOptions.

For example:
new HttpParams({ fromObject: { a, b }, stripUndefined: true }) }

Default behaviour might be that it does not strip undefined.

An interceptor works, but shouldn't be needed for something this simple :)

@camillemoly
Copy link

up please

@JWess
Copy link

JWess commented Aug 24, 2022

NOTE: This would strip out all falsey values, including 0 and empty strings.

Since I use optional parameters quite a bit, I decided to write my own helper function which I have listed below in case it proves useful to others.

In a file named http-params.ts:

import { HttpParams } from '@angular/common/http';

export function createHttpParams(params: {}): HttpParams {
    let httpParams: HttpParams = new HttpParams();
    Object.keys(params).forEach(param => {
        if (params[param]) {
            httpParams = httpParams.set(param, params[param]);
        }
    });

    return httpParams;
}

Then, when I need to create some HTTP parameters, I use it as follows:

import { HttpParams } from '@angular/common/http';
import { createHttpParams } from '../lib/http-params';
...
@Injectable()
export class MyApiService {
  someApiMethod(param1: string, param2?: string, param3?: string): Observable<Member[] | {}> {
    const params: HttpParams = createHttpParams({ param1: param1, param2: param2, param3: param3 });
    ...
  }
...
}

@jmbe
Copy link

jmbe commented Nov 11, 2022

The interceptor workaround appears to no longer work, since any undefined value is now stored as an actual string value "undefined" making the comparison fail.

const params = new HttpParams({fromObject: {a: undefined, b: null}});

console.log(params.get("a"), params.get("a") === undefined); // shows the string "undefined" and comparison fails
console.log(params.get("b"), params.get("b") === null); // shows the string "null" and comparison fails

That is, the value has been already changed to a string before the HttpInterceptor has a chance to see the actual initial value to remove any undefined or null values.

The change was possibly introduced with this commit https://github.com/angular/angular/blame/ec8b52af69a2e04a7b077eb4d4309870ba40e273/packages/common/http/src/params.ts#L168 , which will ensure all values stored with HttpParams are strings.

@GlennTq
Copy link

GlennTq commented Nov 16, 2022

@jmbe same here, we had a also implemented an interceptor that doesn't work anymore.

@xqoma
Copy link

xqoma commented Jun 23, 2023

My solution is to add a new util method to remove "empty" object properties. And it can be used not only for http params.

/**
 * Removes empty properties from an object
 *
 * @param object is any object
 * @returns a clone of the given object with no empty (`undefined` or `null`) properties
 **/
static removeEmptyProperties(object) {
  return _.pick(object, value => value !== null && value !== undefined);
}

We use the lodash library in our project. Here I used the _.pick() method to make it more clear.

That's how I can use it:

find$(searchItem): Observable<any> {
  const url = `${this.URL_BASE}/find/`;
  const params = AppUtil.removeEmptyProperties(searchItem);

  return this.http.get<any>(url, {params});
}

@sanduluca
Copy link

sanduluca commented Feb 12, 2024

Seven years and still counting...

Maybe you should align with the behavior how JSON values are handled in JavaScript:
JSON.stringify({ a:1, b:undefined, c:2 }) produces {"a":1,"c":2}

{a: undefined} is often used as a construct to ignore a JSON key.
If you need a= then '' seems to be semantically good enough.

Maybe {a: null} should be interpreted as ?a param? But definitely not undefined.

Here is how axios is doing it:

https://github.com/axios/axios/blob/638804aa2c16e1dfaa5e96e68368c0981048c4c4/lib/helpers/buildURL.js#L38

They remove both null and undefined from params.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: router breaking changes feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature
Projects
None yet
Development

No branches or pull requests