How to replace String objects with char arrays while still using string methods
You already received good answer about some general ideas for
replacing String objects with C strings. Here I will try to add some
more tricks more specifically targeted to your actual situation.

I can see two places in your program where the use of String shines by
its convenience, and replacing it with C strings will require some
effort. The first is the use of Stream::readStringUntil(char) for
getting the first request line. Here
I see no better option (see edit below) than reading
the stream one character at a time, and putting those characters into an
array:

const size_t input_buffer_length = 256;

// Read a line of text up to the first '\r'.
// Returns a NUL-terminated string, without the final '\r',
// into a statically allocated buffer.
// Warning: this never times out.
const char *readline(Stream &amp;input)
{
  static char buffer[input_buffer_length];
  size_t pos = 0;  // writing position within the buffer
  int c;           // current character
  while ((c = input.read()) != '\r') {
    if (c &gt;= 0 &amp;&amp; pos &lt; sizeof buffer - 1) {
      buffer[pos++] = c;
    }
  }
  buffer[pos] = '\0';  // terminate the string
  return buffer;
}


You would use it like this:

const char *request = readline(client);


For more robustness, you may want to add a timeout. Note also that this
implementation is no reentrant, as it uses a static buffer. This is not
an issue for your use case.

Edit: As noted by hcheung in a comment, you do not need to implement
this readline() function, as you can use
Stream::readBytesUntil(), which handles the timeout
and is provided by the Arduino core:

char buffer[input_buffer_length];
client.readBytesUntil('\r', buffer, input_buffer_length);


The other place is the usage of String::operator+=(const char *) for
generating the response. Here I would advice you against using
strcat() or strncat(): these will require extra memory to store the
concatenated string which you probably do not need. Instead, you can
simply print() the pieces one by one:

void GenerateResponse(const char *text) {
  Serial.println(text);
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println();
  client.println("&lt;!DOCTYPE HTML&gt;");
  client.println("&lt;html&gt;");
  client.print("&lt;br&gt;&lt;h1&gt;&lt;b&gt;");
  client.print(text);
  client.println("&lt;/b&gt;&lt;/h1&gt;&lt;/html&gt;");
  client.flush();
  delay(1);
}


Alternatively, you can make use of the implicit concatenation of string
literals in order to reduce the number of calls to client.print():

void GenerateResponse(const char *text) {
  Serial.println(text);
  client.print(
      "HTTP/1.1 200 OK\r\n"
      "Content-Type: text/html\r\n"
      "\r\n"
      "&lt;!DOCTYPE HTML&gt;\r\n"
      "&lt;html&gt;\r\n"
      "&lt;br&gt;&lt;h1&gt;&lt;b&gt;"
  );
  client.print(text);
  client.print("&lt;/b&gt;&lt;/h1&gt;&lt;/html&gt;\r\n");
  client.flush();
  delay(1);
}